--- title: CLI installation description: Setup, software updates, installed extensions, preferences, completion, and feedback. kind: Reference audience: operators --- import { Callout } from 'fumadocs-ui/components/callout'; These commands prepare local or self-hosted spaces, maintain the installation, and manage workstation preferences. Setup saves configuration by default. Startup requires a separate choice. ## setup [#setup] ```bash cotal setup [--destination ] [-H|--harness ] [--runtime ]... [--project ] [--space ] [--connection-file ] [--configure-only | --start [-d|--detach]] [--on ] [--demo|--full] [-y|--yes] [--json|--plain] cotal setup --skills ``` | Flag | Default | Meaning | |---|---|---| | `--destination` | asked | `local`: a space this machine starts. `self-hosted`: a space registered here that runs on a server, including a Cloud space `cotal login` discovered. `cloud` is refused before anything is written or contacted, and the refusal names that route: `cotal login`, then `--destination self-hosted --space `. | | `--harness`, `-H` | asked | The connector your agents run in. Setup runs its own setup actions and records it as the space's `harness` preference, so `cotal spawn` in the space launches it. When you have no `harness` preference of your own, it becomes yours too. | | `--runtime` | asked; none in a script | A runtime to set up besides the built-in `pty`: `tmux`, `cmux`, `orca`, `herdr` or an installed third-party one. Repeat it for each. | | `--project` | asked | The project folder: the space's root, whose `.cotal/agents` holds its personas. A script's folder must exist; a terminal offers to create it. | | `--space` | asked | The space to prepare (local) or the registered space to use (self-hosted). | | `--connection-file` | none | Self-hosted: register the space from a connection bundle exported where it runs, with the checks `space add --file` makes. | | `--configure-only` | on | Save preparation and start nothing. Refused with `--start`. | | `--start` | off | After successful preparation, start the local stack when needed and launch the default persona. Self-hosted setup uses its registered manager and starts no replacement local stack. | | `--on` | none | Select one self-hosted manager instance for startup. Local setup pins the manager it starts and refuses this flag. In a script, use it with `--start`. | | `--detach`, `-d` | off | With `--start`, leave the managed agent running without opening a terminal view. Required in scripts, JSON and plain mode. | | `--demo` | off | Also seed the guided expert team (`david`, `sven`, `me`) | | `--full` | off | The guided flow with the guided team (implies `--demo`) | | `--yes`, `-y` | off | Ask nothing: every input comes from the flags | | `--skills` | off | Reconcile Cotal skills only through installed connector providers, plus `~/.agents/skills`. Refused with the four inputs, `--runtime`, `--connection-file`, `--configure-only`, `--start`, `--on`, `--detach`, `--full`, `--demo`, `--json` or `--plain`. | Setup saves preparation before a separate startup decision. Configure-only is the default; `--yes` never turns it into startup. On a terminal it asks for each input no flag gave: destination, application, runtimes, project folder and space. It shows the answers before it writes: Save writes them, Change asks every question again with the answers kept, and Cancel saves nothing. Cloud is listed but cannot be chosen. Its hint says to sign in with `cotal login` and choose Self-hosted, because a Cloud space is registered here when you sign in. Without a terminal, or with `--yes`, `--no-input` or `--json`, every input is a flag. A missing one is `USAGE_INVALID` naming each missing flag, before anything is written. The refusal prints the scripted form for the destination and not the full command help. The local form, filled in: ```bash cotal setup --destination local --harness claude --project ~/research --space research --no-input ``` The `self-hosted` form also takes `--connection-file `. A space name `cotal up` cannot start is refused with the name respelled in letters, digits, `-` and `_` as the remedy, and an unknown `--harness` with the known ones and `cotal ext add `. A local space is prepared as `cotal space create` prepares it. A self-hosted space is one already registered with `cotal space add` or a signed-in catalog, or one `--connection-file` registers. A hand-added registration keeps its recorded root. On the first setup of a signed-in catalog space, `--project` binds its project root while its provider-owned sentinel credential stays under the machine-home catalog directory. A later setup must name the same project. Setup then seeds the `default` persona into `/.cotal/agents`, the catalog `cotal spawn` reads for that space, records the application as the space's `harness` preference, runs the application's setup actions, and reconciles Cotal's skills. When you have no `harness` preference of your own, setup also records the application as yours, so a space you prepare later with `space create` uses it. A value you already set stays. A preferences folder another account could write is refused with `PERMISSION_DENIED`, exit 2, before anything is written (see [Preferences](config.mdx#preferences)). The result names the next command: `cotal up` in the project for a local space, or `cotal spawn --space `. `--json` writes one envelope whose `result.written` lists every file setup wrote. Setup needs an installed connector. With none, a `--harness` that names no official connector is refused with `USAGE_INVALID`, exit 1, and otherwise setup refuses with `DEPENDENCY_UNAVAILABLE`, exit 2. Its remedy names `cotal ext seed` on a released install. A source checkout does not seed connectors unless `COTAL_ALLOW_CHECKOUT_SEED=1` marks a sandboxed release test, and the remedy says so. After successful preparation, the terminal asks separately whether to start, and only a typed `y` or `yes` starts it. An explicit `--configure-only` suppresses that question. `--start` skips it; scripts must also pass `--detach`. Missing application/runtime prerequisites keep preparation saved and do not launch anything. ```bash cotal setup --destination local --harness claude --project --space --start --detach --json ``` For a local space, setup invokes the current native `up` in the prepared project, then pins the agent request to that root's recorded manager. Native up returning successfully is not reported as whole-stack health. The managed launch must return its actual joined identity. Self-hosted startup uses the registered manager's persona catalog; `--on` pins its manager instance through credential issuance and launch. It uploads no local persona file. Without detach, the terminal opens that allocated incarnation's native view. If it disappears before attachment, setup reports the confirmed join and an incomplete view, not a healthy attached terminal. Leaving the view sends no stop request. `result.startup` separates the requested self-hosted manager pin, native stack-command return, joined-agent receipt and any classified failure. A declined Start keeps the pin in the suggested spawn command rather than silently selecting another manager. A joined result includes its full manager identity and a usable exact `attach` command when its returned tokens are safe to put in a shell command. Preparation remains saved after a denied or uncertain start. Unknown outcomes are not retried; the setup heading is not shown as healthy and the next command inspects the original startup before attachment. JSON stdout has one envelope. A confirmed local startup refusal keeps its code and diagnostic in `result.startup.failure` and the setup report. Interrupted startup or an incomplete child result remains unknown. Native startup output is not parsed or echoed as readiness evidence. Fresh prepared static-auth, open and plain-loopback user-auth roots use the controller stack described under `up`. Manifest startup retains its native launch path. Runtimes are where agents run. `pty` is built in and always set up. The terminal lists the official runtimes and every installed third-party one, each with whether it is set up and whether the host tool it drives is on PATH, or named by the driver's own variable (`COTAL_ORCA_BIN`, `CMUX_BUNDLED_CLI_PATH`); installed ones and ones whose tool was found start selected. Setup installs each selected runtime as `cotal ext add` does, at this cotal's version, then asks its provider whether it answers, as `cotal runtimes` reports. A selected runtime whose host tool is missing is not installed, and one that does not install or answer leaves the rest saved and the result `partial`. A script sets up only the runtimes `--runtime` names; an unknown name is `USAGE_INVALID` and a missing host tool is `DEPENDENCY_UNAVAILABLE`, both before anything is written. Setup never removes a runtime you leave out; `cotal ext remove` does. `result.runtimes` lists each one with its package, its host tool and whether setup found it set up, added it or left it out. A rerun with the same answers changes nothing and is a `no-op`; a space already prepared, started or registered at that root is reported, not refused. A space name or root another space holds is `CONFLICT`. An application whose host tool is not on PATH leaves the rest saved and the result `partial` (exit 4), naming the tool; an application action that does not finish does the same. `cotal status` reports the machine's state, and it points stale Claude skills and out-of-date `.agents` skills at `cotal setup --skills`. See [Getting started](getting-started.mdx) and, for maintainers, [setup internals](setup-internals.md). ## update [#update] Update preferences are saved in the Cotal home. `cotal update --auto off` disables automatic installation and `cotal update --auto on` enables it on eligible Linux installs. Automatic installation is available on Linux in this release. On macOS, Windows and other platforms, run `cotal update --self` to install manually. Daily checks, update notices and their settings remain available on every platform. Bare `cotal update --auto` reports the saved setting and the platform limit. `cotal update --notify off` silences notices independently, and `--notify on` enables them. `COTAL_NO_UPDATE_CHECK=1` disables both checks and automatic installation for that process. The desktop app's bundled runtime checks for newer versions but never runs npm updates. Its notices point to the signed desktop installer. `cotal update --auto on` has no effect on that runtime; update the desktop app instead. Interactive commands print a one-line update notice on stderr from a daily cached registry check. The check runs daily with `npm view cotal-ai@latest` against your configured registry, and nothing else is sent. A command started during the few seconds an update replaces the installation can fail to load and should be run again. The check runs in the background. JSON, plain, CI and redirected output receive no notice. Version output reports the cached update state without waiting for the registry. Before replacing the global package, the updater verifies both the target and previous release archives and fetches the installed first-party extensions with their dependency closures in a disposable prefix. An unavailable dependency stops the update before replacement. A failed replacement is followed by another reconcile attempt, then a verified reinstall and reconcile of the previous version. A private same-filesystem snapshot restores the previous seed stamp, connectors and operator extensions before Managers start. Interrupted restoration resumes on the next update or daemon start. The snapshot is removed after successful update or restoration. If reconciliation still fails after retry, the command preserves the original child's exit status through restoration. An interrupt takes precedence over that status. If restoration also fails, the error names the installed version and asks for `cotal update --self`; unattended updates remain paused until manual repair succeeds. A daemon that starts while an update is paused keeps running. `cotal daemon status` names the pause and the repair command, and the daemon loads no new version until the repair succeeds. An update the daemon installs on its own runs inside the daemon's service. It reconciles the new version, records the result and exits, and only then does the daemon restart into the new version. It does not wait for that restart, so it cannot restore the previous version if the restarted daemon fails. Read `cotal daemon status` after an unattended update, or run `cotal update --self` yourself to get the verified wait. A runtime bundled with the desktop app refuses `update --self` before changing its installation. Update that app with its signed platform installer so the app, CLI and runtime stay together. ```bash cotal update [--self] [--space ] [--server ] [--creds ] [--on ] cotal update --check [-j|--json|--plain] ``` | Flag | Default | Meaning | |---|---|---| | `--self` | off | If a newer release exists, install that exact validated `cotal-ai` version globally and reconcile through the newly installed binary | | `--space`, `--server`, `--creds` | resolved mesh | Select the running manager whose continuity state is reported | | `--on` | none | Name the manager whose continuity state is reported, by the instance id `cotal ps` prints or, on a user-auth mesh, by its name or host. The reconcile the new binary runs inherits it. | | `--check` | off | Read version availability and recorded extensions without installation or restart; takes no mesh selector or `--self` | | `--json`, `-j`, `--plain` | off | Choose the finite check report; requires `--check` | On a user-auth mesh where you run several managers, the exchange refuses to pick one and names `--on`; pass this machine's manager. Update reads the recorded mesh inventory without refreshing account space catalogues. An expired login to a remote mesh does not block the software update. Local manager continuity checks still apply before changing the installation. After `update --self` replaces the installation, its verified replacement waits up to 120 seconds for the recorded same-installation daemon to load the new version and its intended hosted Managers to answer. It prints this wait before the continuity check. The daemon owns its automatic restart; the updater does not start a competing restart. Connector reconciliation still requires the continuity check to answer. If activation does not finish, the failure names the last daemon or Manager state and asks you to inspect `cotal daemon status` and the recorded user service, restore the connection, then run `cotal update` again. A machine without a recorded daemon service keeps the existing update path. After an upgrade, `up`, `down` and the `service` verbs still control the stack members and first-party service instances an earlier release recorded, even when that release declared their adapter differently, as long as each runs under the stop policy its adapter declares now. A third-party adapter's instances stay pinned to the package version and declaration that started them. Without `--self`, `update` keeps the installed first-party surfaces coherent with the running binary: it verifies each of the six built-in connectors and the web dashboard against the payload the binary ships, byte for byte, and reinstalls one that differs in any way (a missing, changed or extra file, a link where the installed copy should be, an entry at another version, or an extensions prefix whose lock no longer matches what is on disk), brings what a connector installed in its harness to this release (the `cotal` plugin copy Claude Code sessions load, and its Claude Desktop entry), then checks other `@cotal-ai/*` operator extensions against npm's own record in the extensions prefix. One recorded at the binary's exact version with the spec `update` installs (`@`) is kept when the prefix lock records it at that version and every package the lock records is on disk at its recorded version. Any other is reinstalled at the binary's exact version, after a line that says why. A built-in or extension that passes is reported as verified. The extension check reads versions, not file contents. `cotal ext seed --force` remains the unconditional reinstall of the built-ins, and `cotal ext add ` of one extension. Each reinstall runs in an isolated child, so one failure cannot poison later replays. It then checks npm; after successful reconciliation, a newer binary is an informational notice with `cotal update --self` as the next command, not an automatic install. A refused update keeps its recovery guidance instead of suggesting another update that would meet the same refusal. ### Manager continuity If an older updater refuses an answering hosted Manager as `starting`, install the fixed release with npm directly in the same installation prefix. For an installer-managed Unix installation, run `npm install -g --prefix "$HOME/.local/share/cotal" cotal-ai@` using its Node runtime. For an npm-managed installation, use its existing npm prefix. After saving and stopping active seats, restart the recorded daemon service, then run `cotal update` to reconcile matching extensions. The direct npm installation does not call the older updater's Manager-state check. For Managers hosted by the machine daemon, `update` checks every active hosted instance before reconciliation, even with `--space`, because restarting the global installation affects them all. It pins each status request to its instance and refuses unreadable custody or a non-answering Manager. A hosted `legacy` Manager with running agents blocks update because restarting it would end them. The refusal names agents from the Manager's status and gives `cotal stop ` and `cotal ps` commands pinned to their Manager instance and space. The list command requires admin scope on user-auth spaces. Save their work and stop them, then rerun the same update command, including `--self` if you used it. No agent is stopped automatically. During continuity checks, a timed-out remote credential exchange or an HTTP 5xx response retries within one shared 120-second budget. The command reports the host and the remaining retry time. HTTP 4xx responses, including credential denials and rate limits, fail without retry. If the host does not recover before the deadline, restore its connection and run `cotal update` again. Reconciliation still requires a fresh status answer from each pinned Manager. After reconciliation, update waits up to 120 seconds for the matching daemon version and the previously active hosted spaces to report answering Managers. When the daemon version differs, update prepares every hosted child through the local preservation guard before restarting the recorded native service. A refused preparation leaves the occupied Managers running. A direct installed `npm i -g cotal-ai@` replacement is detected by the running daemon. It verifies the replacement entry's reported version and its recorded native service, then asks every hosted child locally to prepare through the existing preservation fence. Occupied legacy Managers keep running while activation waits and retries. Once their seats stop and every child confirms preparation, the daemon stops its owned children and exits. The recorded supervisor then resumes the daemon and its hosting intents automatically. These are local daemon and child IPC operations and require no additional external admin scope. This path does not itself wait for restored Manager readiness. Native restart requests and endpoint readiness do not establish login or reboot autostart on any OS. For Managers outside machine-daemon hosting, `update` reads the selected running manager before disk reconciliation. A machine with no recorded mesh has no running manager to observe, so that read is skipped and the command completes. The same holds when every recorded mesh is down and none is selected. A remote user mesh, registered with `cotal meshes add --mode user`, is named and skipped when no `cotal manager start` or `cotal supervise` process that loaded this install may be serving its space. A supervisor whose command names another install's CLI entry is untouched by replacing this one, so it is no custody here, the same as a manager that runs on another machine. A node binary or a loader argument does not name that entry. A supervisor started without `--space` serves the space its `COTAL_SPACE` names, so a service unit's manager for another mesh does not count as this one's. When such a process runs here, that manager is read like a local one, and a `legacy` verdict's `Next` line names `cotal manager stop --space ` as the stop before the cold update. A `legacy` verdict still comes only from a manager this machine read. A stopped mesh that this machine runs, chosen with `--space` or by running inside its project, has no running manager either when no `cotal supervise` process that loaded this install may be serving its space and nothing listens at its recorded broker address, so the read is skipped there too. A controller stack that completed ordinary `cotal down` is eligible after its retained service records prove every process gone. Missing instance records, live or unresolved processes, and an unfinished down still refuse. That is the cold path past a `legacy` verdict: `cotal down`, then `cotal update`, and the verdict's `Next` line names it with that mesh's space. A space prepared on this machine that never started, whether this folder's, selected or named, has no broker and no manager, so it is named and skipped the same way. Any other selected mesh whose manager cannot be read is still a refusal: a named space that is not recorded, a mesh registered with `cotal meshes add`, a `--server` override, a `cotal supervise` process still running from any folder whose command line names that space or no space, or a broker address where anything accepts the connection or the dial does not finish in time. With several meshes running and no `--space`, `--server` or `--creds`, the install is machine-wide, so every running manager is reported in turn, each under its space name, before anything is written; a `legacy` verdict on any of them makes the whole run not a hot update. A folder that holds several registrations, such as two remote spaces registered there with `space add --from`, is read the same way, one registration at a time. A selector flag still reports one manager. A manager without a custody generation is reported as `legacy`: it cannot preserve its manager-owned PTYs, so the command says that this is not a hot update and prints `exact`, `fork`, `fresh`, or `drain-only` for every seat. This report sends no stop, preservation-commit, or replacement command. It does not preserve a running PTY on a legacy manager. The built-in pty runtime spawns in-process on every platform and reports `legacy`. On Linux it still adopts seats that an earlier manager left under a detached custodian, but it starts no new custodian. An incompatible native `@lydell/node-pty` or ConPTY ABI break remains an explicit per-seat maintenance cut. ### CLI upgrade With `--self`, the selected running manager is reported before any global install. When a newer release exists, Cotal first downloads and integrity-checks its `cotal-ai` archive and the archives of its first-party runtime dependency closure through npm, using a temporary cache and no install scripts. Each registry request has a 15-second timeout and no retries. Missing archives or invalid metadata refuse before global installation or connector reconciliation, with a registry propagation hint and a request to check registry access and try again in a few minutes. This preflight does not guarantee later registry requests or third-party dependency downloads will succeed. Cotal then installs the exact version it validated, resolves and verifies that package in npm's global root, then launches that binary with the same `--space` / `--server` / `--creds` selection to reconcile connectors and first-party extensions to the new generation. An npx or dev-clone invocation therefore installs and continues through a separate global copy; it never claims the already-running process changed. From a source checkout, the `update --self` hint in `update` and `update --check` says so. If the binary is current, `--self` performs the normal local reconcile without reinstalling it. Each npm install it runs starts with 022 added to your umask, so a umask of 0002 does not leave the installed tree group-writable. On POSIX, an installed npm CLI binds the update to the prefix whose package and `bin/cotal` link identify the running entry. This also applies when a service invokes the package bin without the installer's external launcher. The vendored Node runtime's own npm prefix is not used. A missing or redirected global bin link, a conflicting `npm_config_prefix`, or an npm root that disagrees with that prefix refuses before installation. Local/npx copies and source checkouts still install a separate global copy; Windows keeps npm's configured global root. Once global replacement starts, Ctrl-C waits for installation, verification and reconciliation to finish before returning exit code 130. SIGTERM waits the same way and returns 143. The command explains the wait, keeps its update lock and does not send terminal interrupts into the child processes that are replacing packages. An install or reconcile failure still reports its own nonzero result. Force-killing the process or closing its terminal is not a supported cancellation. Run through the installer's `cotal` launcher, `update` first removes group and other write from the folders and files you own in Cotal's install root, without following links. An installer from before it set its own umask, run under a umask of 0002, left that tree group-writable. Running the installer again repairs it the same way. `update` also removes group and other write from the folders and files you own in Cotal's config directory, `$XDG_CONFIG_HOME/cotal` or `~/.config/cotal`, which holds the extension prefix and the seed store. An earlier seed run under a umask of 0002 left those folders group-writable, and the web seed's check for other local writers refused them. Each repair prints how many entries it changed and where. A folder above the config directory, such as `~/.config`, is not Cotal's, so `update` does not repair it. `update` refuses that folder before any repair, seed or binary replacement, and names `chmod g-w ` as the next step. ### Update limits Third-party extensions are listed with their installed version and recorded spec but are not auto-updated in v1. Floating third-party updates require `@cotal-ai/*` peer-range validation and are a future follow-up. A failed connector/extension install, npm metadata check, or requested global install is reported and makes the command exit nonzero. Independent extension attempts continue so the output includes every failure; an unavailable npm registry does not undo a completed local reconcile, but the command still exits nonzero because it could not establish that the install is current. ### Update availability [#update-availability] `cotal update --check` queries the latest registry version and reads recorded extension versions. It performs no install, reconcile or process restart and takes no mesh selection. It supports `--json/-j` and `--plain`. The registry read runs npm with a temporary cache that is removed afterwards, no log files and no npm update notifier, so it leaves nothing in npm's cache, log directory or notifier stamp. Missing registry data or an unreadable extension inventory remains unknown or partial, never an empty successful check. The report compares first-party recorded versions with the running CLI. It does not inspect package contents, signatures or loaded process generations. `cotal update` and `update --self` keep the existing guarded update, continuity checks, download verification and current-generation reconciliation. Their installer progress is native output, not a structured check receipt. ## join [#join] ```bash cotal join --space --name [--role ] [--channel ] cotal join --link | --token ``` | Flag | Default | Meaning | |---|---|---| | `--space ` / `--server ` / `--creds ` | resolved mesh | Which mesh, and which credential | | `--name ` | none | Your presence name | | `--role ` | none | Your role | | `--channel ` | link's first channel, else `general` | Channel to join; a static credential needs a read grant for it | | `--kind ` | `agent` | Endpoint kind | | `--link ` | none | Join link (`cotal://…`) | | `--token ` | none | Join token | | `--lifecycle-uid ` | none | Required with `--creds`: the lifecycle UID minted alongside the credential (`COTAL_LIFECYCLE_UID` works too). A credential's durable grants name exact lifecycle-keyed resources, so `join` refuses to invent one | | `--tls` | off | Connect over TLS | | `--expires-in `, `--expires-at ` | no expiry | A bare join on a static mesh only, one or the other: when the session's issued credential ends. Without either it has no expiry, and it is retired when you leave. Refused with `--link`, `--token` or `--creds`, and on a user-auth or open mesh | An interactive presence: join a space under your own name and role, without launching an agent harness. A `--link` supplies the where and the auth in one value. See [Spaces](spaces.mdx) and [Identity and auth](identity-and-auth.mdx). A `--token` or `--creds` goes to `--server`, or else to the broker the space is registered at, keeping that registration's TLS requirement. With no `--space`, that is the selected space, else this folder's, and inside a project whose root has one space other than the selection, that space. A space not registered here is refused before anything is dialed. The default address is never assumed for a credential. A `--creds` join needs a provisioned inbox and a read grant for its landing channel. For `general`, the space's owner mints the credential with: ```bash cotal auth mint --provision --allow-subscribe general --allow-publish general --out cotal join --space --creds --lifecycle-uid --channel general ``` The owner hands over the file and the lifecycle UID that mint reports. `--allow-subscribe` grants reading; `--allow-publish` is optional when the session will not post. Use the intended channel in both commands. `--provision` alone grants no channel access. A missing read grant is refused with `PERMISSION_DENIED` naming the channel and `--allow-subscribe`. A mismatched lifecycle UID is refused separately, naming the UID and the mint that reported its pair. Neither refusal points at the delivery daemon. A credential minted without `--provision` has no inbox there; with its paired UID and a read grant, the refusal names that lifecycle and the mint that makes one. Without a UID, `join` first refuses with the lifecycle-pairing requirement. On a registered user-auth space, sign in with `cotal login` first. `join` uses your existing `cli` actor bearer and renews it through the same login and exchange while the session runs. Its channel and DM resources must already be provisioned by the space operator. It never mints static credentials or widens your grants. A changed principal, lifecycle or connection trust refuses renewal and requires leaving and joining again. A revoked login is reported by renewal, and the current connection can last only until its already-issued bearer expires. For `nats://` and `tls://` addresses, if TLS is required but the broker does not offer it, `join` refuses with a request for the operator's TLS-enabled broker address. It never retries over plaintext. A `ws://` address uses plaintext WebSocket. Use `wss://` for TLS. `join` has no `--json`. A refusal before the session starts prints on stderr with its code and exits 1 for `USAGE_INVALID` and 2 for any other refusal: `TARGET_NOT_FOUND` for a space not registered here, a creds file that does not exist, a lifecycle with no inbox or a space not provisioned for a console session, `PERMISSION_DENIED` for credentials the broker rejects, `TARGET_UNAVAILABLE` for a broker that does not answer, and `UNSUPPORTED` for a broker without the TLS the join requires. A running join that cannot come back ends the same way, with its reason and its Next. When it re-establishes its connection and finds its inbox gone from the broker, as after `reset store` while it was disconnected, it ends `TARGET_NOT_FOUND` and says to run the same `cotal join` again, or, for a `--creds` join, to mint the credential again with `--provision`. When the broker rejects its credential for good, as after `reset space`, it ends with the refusal a one-shot command with that credential gets, as `history --follow` does. A user-auth join keeps re-establishing in both cases, and an open or `--token` join when the broker rejects it. A broker that comes back on an empty store soon enough for the client to reconnect by itself does not end the connection, so the join keeps running without its DM inbox, and direct messages to it are not delivered. After a second check it prints one warning that its DM inbox is missing, on a line that starts with `!` like its other warnings, and keeps checking. If the inbox does not come back, interrupt the session and join again. A `--creds` join first needs the space's owner to mint the credential again with `--provision`. ## ext [#ext] ```bash cotal ext [list] [--json | --plain] cotal ext add cotal ext remove cotal ext root # print just the install prefix (scriptable) cotal ext seed [--repair|--reset|--force] ``` Operator-installed extensions: `add` installs an npm package into a cotal-owned prefix and records every registry provider it contributes. Commands appear in help, completion, and dispatch; runtime providers are loaded on demand by commands such as `supervise`, and by a running manager the first time a managed seat names one with `spawn --runtime `, so a `pty` manager launches a seat on an installed `tmux` without a restart; local process providers participate in `status` and selective `down`. Removing an additional runtime extension, one the manager was not started with, refuses the next seat that names it; the manager's own runtime and the seats already running stay as they are. `remove` and `list` manage them. The `@cotal-ai/web` dashboard is the canonical service preset and local-process example. Installed packages and their location are described in [config](config.mdx). When a package needs an export its linked `@cotal-ai/*` peer does not have, `add` rolls back and names which install is behind, as a later load of an installed one does. Each load or add reads the installed package's metadata once, using that same result for its version, peer links, entry and import diagnosis. Corrupt or unreadable metadata refuses before import; an entry that later changes its own `package.json` cannot change the peer range used to diagnose that import. When linking siblings, only a missing package directory can be skipped with a warning. A directory without metadata, or a dangling package link, is damaged and still refuses. Removing an extension or cleaning up a failed add keeps unrelated file-based extensions as installed copies, rather than turning them into links to their source or the built-in seed store. Bare `cotal ext` lists the inventory, headed by the install prefix. That prefix is a cotal-owned npm root kept **separate** from npm's own global tree. These packages never show up in `npm list -g`, `cotal ext` (or the Extensions section of `cotal status`) is the canonical inventory. `cotal ext root` prints only the path, for scripts. The versions shown are the manifest pin recorded at add time. On POSIX, cotal creates a missing Cotal config directory with mode `0700`, even with a group-writable umask, whichever command writes there first. The extension writer creates a missing install prefix with mode `0755` and its `package.json` with mode `0644`. Its npm children start with 022 added to your umask. The extension writer leaves existing directory permissions as they are, and [`update`](#update) removes group and other write from the config directory and everything you own under it. Completion still refuses a pre-existing writable ancestor rather than weakening its shell-file ownership checks. Bare `ext --json` (or `ext list --json`) writes one JSON document: `result.root` is the install prefix and `result.items` holds one row per installed extension with `pkg`, `version`, `spec` (what `ext add` was given), `seeded` (`true` for an entry the built-in seed installed) and `provides` (the `kind:name` refs the table shows). An empty prefix is an empty list. `--plain` writes the same report without color. The other `ext` subcommands refuse `--json` and `--plain` with `USAGE_INVALID`. `ext root` stays a raw path for `$(cotal ext root)`. An installed package whose directory is gone is reported as not installed. One whose `package.json` is not JSON or has no `version` is reported as corrupt, naming the package and its `package.json`. `add`, `remove` and each built-in seed relink the shared peers of every installed package after npm changes the prefix. A package recorded in the manifest whose directory is gone does not fail that step. One example is a package npm pruned as extraneous because no dependency list names it. A warning names the package and its path, the command that reinstalls it and `cotal ext remove `, which drops its entry. For a built-in the reinstall command is `cotal ext seed --repair`. For any other package the warning prints `cotal ext add ` with that placeholder, since a recorded spec can be a URL that carries a credential. Replace it with the spec you first passed to `ext add`. The entry stays in the manifest until you run one of them. A seed or repair that succeeds past it neither installs the package nor removes its entry, so `cotal ext list` still lists it and a command that reads that entry can still report it as a problem. A package directory that is present without a readable `package.json` still fails the step. After a seed that failed this way in an earlier release, `cotal ext seed --repair` finishes it, and `cotal ext remove` then drops the entry. Removing an extension that owns a running local process is refused with its process or record remedy. There is no command that stops one extension's legacy process. Follow the emitted `stop pid , then retry`, `delete , then retry`, or resolve instruction before retrying removal, so uninstalling does not strand a process whose provider is gone. While it uninstalls, `remove` reserves each process record the package declares under every mesh root, and a record folder it creates there is mode `0700`, even with a group-writable umask. Loading an installed extension takes the same locks as `add`, `remove` and `cotal update`. When another `cotal` process holds them, the load waits up to five seconds in total (one deadline for both locks) without blocking the waiting process, then refuses. A running `cotal update`, `cotal ext add` or `cotal ext remove` holds the update-pass lock as well as the install/remove lock, so the load refuses with "another extension update or mutation is in progress (pid N) - retry once it finishes". The install/remove text, "extension install/remove is in progress (pid N) - retry after the active `cotal ext` command finishes", appears only when the update-pass lock is free but a live process still holds the install/remove lock itself, for example an update child that outlived its `cotal update` parent. Re-run the command once the other one has finished. ### Connector seeding [#built-in-connectors-are-seeded-extensions] The first-party agent connectors (`claude`, `opencode`, `codex`, `hermes`, `jcode`, `pi`) are not compiled into the binary. They are seeded on first run through the **same** `ext add` path a third party uses, and appear in `cotal ext list` like any other extension. So you can remove one you do not want (`cotal ext remove @cotal-ai/connector-hermes`), and a deliberately-removed connector STAYS removed across upgrades. `cotal ext add ` adds a third-party connector the same way. The web dashboard (`@cotal-ai/web`, providing `service-preset:web`) is the seventh built-in seeded on the same path. `cotal ext seed` is the maintenance entry for that seeding (it runs automatically before the first command of each boot that is not a read, so you rarely call it; `cotal up` runs it when it first starts a process). A read, such as `status`, `ps`, bare `use`, `ext list` or `docs`, installs and refreshes nothing. An installed extension's `reads` declaration is cached when it is added. A bare read and a read subcommand skip that seed prelude; a subcommand without the declaration still seeds. After an upgrade, a read that would load a built-in still at another version names the package and its version with `cotal ext seed` instead of loading it. Each seeded connector's `✓ added` line goes to stderr, so the command that triggered the seed keeps stdout to itself: | Flag | Meaning | |---|---| | (none) | Reconcile: seed any never-seeded built-in, refresh a seeded one whose version the binary bumped, leave a removed one removed. A no-op once current. | | `--repair` | Recover after a seed that failed or was interrupted, or a lost authority (rebuilds that seed's connector; restores the removed-vs-never-seeded record from its durable backup). Also reinstalls a seeded built-in whose entry file is gone or whose `package.json` is not JSON or has no `version`. | | `--reset` | Discard the record and re-seed all seven built-ins (the six connectors plus the web dashboard). **Resurrects any you removed.** Rebuilds cleanly over corrupt seed state. | | `--force` | Re-seed the built-ins even when the version stamp is current. Refuses to downgrade a newer shared store. | A seed that fails names the connector and the reason, such as npm missing from PATH. Until `cotal ext seed --repair` finishes it, each command that seeds refuses. The refusal says whether the last seed failed, and why, or was interrupted. When a newer `cotal` advances the operator-global seed store to its generation, it prints one migration line naming the old and new generations, the exact CLI entry that wrote the store, the commit timestamp, and `seed/stamp.json`. That writer and timestamp are kept in the stamp, so a later older CLI refusal can say which executable wrote the generation it will not overwrite and when. Legacy generation-only stamps remain readable; their refusal simply has no writer provenance to add. An older `cotal` refuses a seed store written by a newer version. The refusal names the store's `seed` folder and the `extensions` folder beside it, and says to restore both from a backup taken before the store moved to the newer generation; the two versions are context. When it can verify a sufficient `cotal` executable on PATH or at the installer's `~/.local/bin/cotal` location, the refusal also names that absolute path, which uses the store as it is, so a reduced service PATH does not select the older binary again. Every maintenance mode, including `--force` and `--reset`, refuses to replace a newer shared generation. Same-generation force remains supported. Reset still repairs corrupt state and resurrects removed connectors. Older released executables may still allow a backward rebuild; do not use their force advice to recover from selecting the wrong installation. A source-checkout CLI (`pnpm cotal`, `tsx bin/cotal.ts`, `node bin/cotal.ts`, or a suite child of those) refuses to write or garbage-collect that store. The refusal names the path, the generation it declined, and `COTAL_SKIP_CONNECTOR_SEED=1` as the way to run other commands from a checkout, because pointing `$XDG_CONFIG_HOME` at a scratch dir alone does not lift it. With the skip set, a fresh config gets no built-in connectors (`cotal ext list` shows none), so add one from the checkout with `cotal ext add /extensions/connector-claude-code` against that scratch `$XDG_CONFIG_HOME`. `COTAL_HOME` does not relocate this store. An entry that cannot be proven as a released install is refused the same way. Isolated release tests that must seed from a checkout-shaped `bin/` set `COTAL_ALLOW_CHECKOUT_SEED=1` after pointing `$XDG_CONFIG_HOME` at a scratch dir; that override is documented here, not on the refusal line. An opt-in write still records the checkout path in `seed/stamp.json` as `writtenBy`. The default connector for a bare `cotal spawn` (no `--agent`) is the persona's `agent:` pin if it has one, else `claude`; set `COTAL_DEFAULT_AGENT` or `cotal config set harness` (e.g. `opencode`) to change the fallback. It is a default, so a persona that pins its harness still wins over it. An `--agent` naming a removed connector fails loud with the exact `cotal ext add` to restore it. Set `COTAL_SKIP_CONNECTOR_SEED=1` to turn off the automatic first-run seed/refresh entirely (for a controlled or offline setup that manages connectors by hand); `cotal ext seed` still runs on request. `cotal agent-bearer` never takes the seed at all: it is exec'd by spawned seats on every bearer refresh, so it neither reconciles nor is refused by the store's generation (see [Plumbing](cli-auth.mdx#plumbing)). `cotal completion` skips the seed as well, so a shell profile that sources its stub never reconciles and is never refused by the store's generation or the source-checkout guard. Printing a stub writes nothing, and `completion install` writes only shell configuration. ## docs [#docs] ```bash cotal docs [] [--refresh] [--json | --plain] cotal docs --search [--json | --plain] ``` | Flag | Default | Meaning | |---|---|---| | `--search ` | none | Show the sections that best match a query, each naming its page | | `--refresh` | off | With a page: also try the version-pinned copy on docs.cotal.ai | | `--json` / `-j` | off | One JSON document | | `--plain` | off | The same report without color, and a page as its Markdown | Reads the docs bundled with this installed version, offline: the same pages agents read with `cotal_docs`. Bare `docs` lists the pages by category (`Start here`, `Guide`, `Concept`, `Reference`, `Project`, the order of the docs index), each with its title; `result.pages` holds each page's slug, title, kind (its classification line, such as `Concept (informative)`), category (the heading it lists under) and summary, in that order, and `result.sources` the normative `spec`, `lang` and `schema`. A page is named by its slug, ignoring case and a trailing `.md`; `result.body` holds its Markdown. On a terminal a page is drawn for reading: each heading is a title, paragraphs and lists wrap to the terminal, a table is text that becomes one record per row when its columns do not fit, and a code block keeps its lines. Links show their destinations, including page names and section anchors. A page reference such as `` can be opened with `cotal docs cli-processes.mdx#daemon`. To a pipe or a file, and with `--plain`, the page is its Markdown. An unknown page is refused with `USAGE_INVALID`, naming every page. `--refresh` fetches the page from the immutable path for this version on docs.cotal.ai. `result.source` says which copy was served, `bundled` or `docs.cotal.ai`, and `result.refresh` says what the fetch did: `served`, `not-published` (the path has no copy) or `unreachable`. The bundled copy is served whenever the fetch does not succeed. `--refresh` applies only to a page. `--search` ranks sections by the words of the query. A search with no match is an empty result. ## config [#config] ```bash cotal config [--user | --space ] [-v | --verbose] [--json | --plain] cotal config get [--user | --space ] [--json | --plain] cotal config set [--user | --space ] [--json | --plain] cotal config unset [--user | --space ] [--json | --plain] ``` | Flag | Default | Meaning | |---|---|---| | `--user` | your defaults for a write | Your own values, with no space's values | | `--space ` | the space a launch here uses, for a read | Your values for one space | | `--verbose` / `-v` | off | Bare `config` also shows the preferences file and its revision, which its JSON result always holds | | `--json` / `-j` | off | One JSON document | | `--plain` | off | The same report without color | Your [preferences](config.mdx#preferences): `harness`, `persona`, `detach-key`, `color`, `editor` and `name`. Bare `config` lists every key with the value a command uses, where it came from (a variable, your value for the space, your own value, or the default) and the values it outranks. `get` shows one key. A read looks through `--space`, else the space a launch here uses, the space `spawn` resolves from this folder. A running space registered for this folder is that space, even when `cotal use` selected another. With no space running here it is the selected space, or the only running one. With none of those it reads your defaults. `--user` reads your defaults alone. `set` and `unset` change your defaults, or your values for one space with `--space`. Only `harness` and `persona` have per-space values. `set --space` takes a space registered or prepared on this machine, and refuses any other name with `TARGET_NOT_FOUND` (exit 2) and the known spaces. A read and `unset --space` also take a space that still holds a saved value, so a value left for a space you removed can be read and removed, and refuse any other name the same way. A write that another value outranks in this shell is still saved, with a `PREFERENCE_SHADOWED` warning naming that value: a variable, or your value for the space a command here uses when you write your defaults, with or without `--user`. The receipt's effective value is the one a command here uses, read through `--space`, else that space. A `persona` that the space's catalog does not hold is saved with a `PERSONA_NOT_FOUND` warning naming the catalog. A variable whose value `config set` would refuse still wins, and `config`, `config get` and `status` name it in a `PREFERENCE_INVALID` warning with the reason. Setting a value that is already saved, or unsetting one that is not, is a `no-op` and exits 0. An unknown key or an invalid value is refused with `USAGE_INVALID` and the valid choices, and a write that lost a race with another writer is refused with `CONFLICT`. A preferences folder you cannot write is refused with `PERMISSION_DENIED` (exit 2), naming the folder, before anything is changed. So is a folder another account could write, with the `chmod` that fixes it, and a linked `preferences.json`, which `config set` never replaces (see [Preferences](config.mdx#preferences)). `result.settings` (or `result.setting` for `get`) holds each key's `value`, its `winner` (`{source, name, value}`, where `source` is `environment`, `space`, `user` or `default`) and the `shadowed` layers below it. A write's result has the `previous` and new `value`, the file's new `revision`, and `effective`, the key as a command there now resolves it. ## completion [#completion] ```bash cotal completion [--json|--plain] cotal completion script cotal completion install [shell] [--json|--plain] cotal completion uninstall [shell] [--json|--plain] ``` ### Completion inspection Bare `completion` reads local integration files. It reports the script version, startup reference and recorded ownership separately. It does not inspect or load the running shell. An unreadable file or ownership record produces a warning and a partial result, exit 4. The warning names the file and why. A record wired into another startup file, as an install under another `ZDOTDIR` leaves one, names that file and the `ZDOTDIR` to read it with. A folder above the files that is a symlink, owned by another account, or writable by an account that is not you is named once, with the `chmod` that fixes it when one does. Group write is accepted when the group is your proven user private group and the folder has no extended ACL, the same rule as every other protected path. The files under it are not opened, and one that does not exist is reported missing, so a report with nothing installed stays complete, exit 0. Next offers that `chmod` first, offers `install` and `uninstall` only when nothing stops them, and offers `uninstall` only when something is installed. A relative `ZDOTDIR` is resolved from the current directory for this report, as zsh resolves it. In zsh, initialize its completion system with `autoload -Uz compinit && compinit` before sourcing the native zsh stub. Load completion in the current shell: ```bash source <(cotal completion script bash) source <(cotal completion script zsh) cotal completion script fish | source cotal completion script powershell | Out-String | Invoke-Expression ``` `completion script` writes raw shell code and takes no `--json` or `--plain`. Generation is offline. The stable hidden `__complete` entry reads registered command specifications, the spaces registered or prepared on this machine, the extension manifest, the bundled docs index and local persona files. It does not contact the mesh or refresh extensions. Every `--space` offers the spaces `cotal use` offers, and every `--creds` lets the shell complete a file name. `space show`, `forget` and `delete` offer the spaces registered or prepared here. `config` offers its keys, and `config set color` and `config set harness` the values a write accepts. `docs` offers its pages, `watch --view` its views and `space add --mode` its modes. A channel name for `send -c`, `history` or `channel show`, `members`, `set` and `remove` comes from the channels the target space's personas declare, as `spawn` reads them, so a space prepared and not started offers its own. The broker keeps the others, so a persona that declares only `>` offers none. A candidate that holds a control character, such as a persona file name with an escape sequence or a newline in it, is not offered. `persona` still lists that file with the characters escaped. A persona catalog folder that cannot be read fails the completion of a persona name, or of a channel or role its personas declare, so the shell offers none, and with `COTAL_COMPLETE_DEBUG=1` it prints the folder. A persona file that does not parse fails a channel or role completion the same way, and that flag names the file, because the declared set would be incomplete. A record it skips, such as a prepared space record that does not parse, is named on stderr only under that flag too. The stubs discard stderr, so run the completion by hand to see it, as `COTAL_COMPLETE_DEBUG=1 cotal __complete persona show ''` does. The bash and zsh stubs complete file names where a value takes a path. The fish stub offers only cotal's own candidates. In bash and zsh, a file argument such as `team check `, `up -f `, `--channels `, `--creds `, `run start --file ` or `auth mint --out ` completes from the shell's own file names, and so does a folder such as `space add --root `. A value with a fixed set of choices, a duration or free text does not offer files. A stub installed by an earlier release offers no files. `cotal completion` reports it as differing from this version, and `completion install` refreshes it. ### Installation paths `install` and `uninstall` take a shell name or detect it from `SHELL`. Pass `powershell` explicitly when PowerShell inherited another shell's `SHELL`. A `SHELL` that names a shell completion does not support, such as tcsh, is refused with `UNSUPPORTED` (exit 2), naming it. On supported POSIX hosts: - Bash uses `~/.config/cotal/completion.bash` and an owned block in `~/.bashrc`. - Zsh uses `~/.config/cotal/completion.zsh` and an owned block in `$ZDOTDIR/.zshrc`, or `~/.zshrc` when `ZDOTDIR` is unset. - Fish uses `~/.config/fish/completions/cotal.fish`, which fish loads automatically. - PowerShell on Linux or macOS asks the installed `pwsh -NoProfile` for its current-user all-hosts profile and wires an owned `completion.ps1` stub into that file. A missing shell, invalid profile reply or missing profile directory refuses without guessing a path. An absolute `XDG_CONFIG_HOME` replaces `~/.config` for the owned stubs; a relative value is ignored. Mutation refuses a relative `ZDOTDIR`, missing startup directory, symlink, untrusted owner or unsafe file access before it creates anything. A missing startup directory is named, with `ZDOTDIR` when that variable chose it, and with the `mkdir` that creates it. One refusal names every refused startup file, stub, record and directory on the way to them with its reason, and one another local account could write with its mode and one `chmod` that fixes them all. Bare `completion` offers that `chmod` before `install`. Group write on a file you own is accepted when the group is your proven user private group and the file has no extended ACL. Replacements keep the existing file's group and mode, including under a setgid directory. The temporary file starts private, and its final access is checked before it receives shell content. A file you do not own stays refused. A startup file that is a symlink, as a dotfile manager such as stow links it, is never written through: `install` refuses it, exit 2, names it and gives the line to add to the file it points to yourself. Bare `completion` warns that it is a symlink and does not read it. A startup file its owner made read-only is never replaced. `install` still writes its stub, then fails, exit 5, and names the line to add to that file yourself. `uninstall` refuses it, exit 2, and changes nothing until you remove that line. On Windows, PowerShell script generation works but automatic profile editing remains unsupported: the guarded POSIX file writer cannot safely replace Windows reparse-point paths. Installing files does not establish that the host runs its profile or permits scripts under its execution policy. ### Completion ownership Installation writes a private `.ownership.json` record beside its stub. Reinstall refreshes only an unchanged recorded stub. Uninstall removes only that stub and the unchanged recorded startup block, keeping other startup bytes. A startup file that `install` created is removed when nothing else is left in it; one that was there before stays, even empty. Modified or unrecorded material is preserved with a refusal. For a recorded stub or startup block you edited, the refusal names `--force`: `install --force` rewrites the stub and each block from `# >>> cotal completion >>>` to `# <<< cotal completion <<<`, and `uninstall --force` removes them. Startup content outside those markers is kept. A block whose end marker is gone is still refused, with the file named, because only you can tell which lines are cotal's; remove them there yourself. For an integration you modified, bare `completion` offers `install` and `uninstall` only with `--force`. A stub from an earlier release without an ownership record needs manual review; matching generated text alone is not treated as ownership. An unowned startup reference to a managed stub must be reviewed before removing the stub. These commands cannot unload functions in a parent shell. Cotal writers use a per-shell lock and recheck file identity and bytes before changing them. An unconfirmed filesystem mutation is reported as unknown and is not retried. A confirmed change followed by another refusal is partial, with the acknowledged file paths retained. This is not a filesystem compare-and-swap against an editor that ignores the lock. `update` refreshes existing unchanged recorded stubs through its current-generation CLI. It never adds startup wiring or reinstalls a removed stub. Modified and unrecorded integrations are kept; any guarded refresh it cannot complete is reported. `update --check` does not refresh completion. The old `completion ` form is a usage error. Custom startup commands need manual migration; see [Upgrading](UPGRADING.md#shell-completion-script-in-0910). No command silently repairs custom wiring. Install and uninstall support one finite JSON document or the plain report. ## feedback [#feedback] ```bash cotal feedback "" [--type ] [--email ] [--details ] [--json|--plain] cotal feedback "" --key-file [--json|--plain] ``` The type is bug, idea, friction, praise or other; severity is low, medium or high. Optional area and contact name are included only when supplied. The public intake requires a traceable email, from `--email`, the existing configured contact or local git email. No environment setup is required. Do not put credentials, private transcripts or unrelated customer data in feedback. A protected `--key-file` selects keyed intake without placing a credential on argv. The file holds one key: an opaque bearer token with no whitespace, optionally followed by one newline. An existing internal intake configuration remains supported. The old raw `--key` input refuses before the request and does not echo its value. Private key files must meet the shared file-ownership and protection policy; unsupported environments refuse rather than weakening that policy. `--url` can select a self-hosted intake. A Cotal intake accepts reports only at `POST /v1/feedback`, so the URL names that path, such as `https://intake.example/v1/feedback`; the origin alone gets HTTP 404. Remote intake requires HTTPS; HTTP is limited to literal loopback development hosts. URL credentials and fragments are refused, and redirects are not followed. One request has a 20-second deadline and is never automatically retried or rerouted to a different credential/intake. A connection that never opens sends nothing: a refused connection, a failed DNS lookup or a failed TLS handshake refuses with `TARGET_UNAVAILABLE` and the native error code. A timeout or a broken connection after the request was written, or an unconfirmed server outcome, can mean the report arrived: inspect the original intake before sending it again. An intake refusal names its cause and a next step. HTTP 401 means the key is missing or refused, 404 means the URL is not the intake's feedback path, and 429 means the intake is rate limiting submissions. A 429 names the retry time when the intake's `Retry-After` header gives one. JSON/plain receipts report the intake origin, HTTP acceptance and a bounded record ID when the server provides one. They do not repeat the submitted summary, details, contact values, key or response body. Acceptance does not prove that anyone read the report or acted on it. A success response without an established ID is reported with that limitation, not retried for a new ID. Run a self-hosted intake with [`feedback-intake`](cli-services.mdx#server-daemons). ## telemetry [#telemetry] ```bash cotal telemetry [--json|--plain] cotal telemetry off|minimal|regular [--json|--plain] ``` The CLI sends anonymous usage data: one small event per command as it exits, and at the default `regular` level one install summary per day. [Telemetry](telemetry.mdx) lists every field and what is never sent. The bare form prints the level in force, what set it (the settings file, `COTAL_TELEMETRY`, `DO_NOT_TRACK` or `COTAL_TELEMETRY_DISABLED`), the install id, the intake URL and the settings file. A level argument writes the file; `on` is an alias of `regular`, and writing the level the file already holds is a `no-op`. A level the environment forces still wins, and the report says so. Under `DO_NOT_TRACK`, `COTAL_TELEMETRY_DISABLED` or `COTAL_TELEMETRY=off` the file is neither read nor created, so a level argument saves nothing and is a `no-op` with an `ENVIRONMENT_OPT_OUT` warning. Any other word is refused `USAGE_INVALID`, naming the levels. `cotal telemetry` itself sends nothing. The first interactive run at `regular` prints one notice to stderr. `COTAL_TELEMETRY` set to anything but a level refuses `USAGE_INVALID`. A settings file that does not validate refuses `SCHEMA_INVALID` on `cotal telemetry` and on every command that reports usage, until it is fixed or moved away. Under the environment opt-outs above nothing reads it, so it stops no command. ### Preparation [#preparation] See the [current command contract](cli-installation.mdx#setup) for this behavior. ### Startup [#startup] See the [current command contract](cli-installation.mdx#setup) for this behavior. ### Runtimes [#runtimes] See the [current command contract](cli-agents.mdx#runtimes) for this behavior. ### Setup outcomes [#setup-outcomes] See the [current command contract](cli-agents.mdx#runtimes) for this behavior. ### Reconciliation [#reconciliation] See the [current command contract](cli-installation.mdx#update) for this behavior. ### Provider loading [#provider-loading] See the [current command contract](cli-installation.mdx#ext) for this behavior. ### Extension inventory [#extension-inventory] See the [current command contract](cli-installation.mdx#ext) for this behavior. ### Extension mutations [#extension-mutations] See the [current command contract](cli-installation.mdx#ext) for this behavior.