docs/cli-installation.mdx
Rama main · réplica de lectura del repositorio original.
---
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 <local|self-hosted>] [-H|--harness <connector>] [--runtime <name>]... [--project <dir>]
[--space <name>] [--connection-file <bundle.json>] [--configure-only | --start [-d|--detach]]
[--on <manager-instance>]
[--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 <name>`. |
| `--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 <bundle.json>`. 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 <npm-package>`.
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 `<project>/.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 <name>`. `--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 <dir> --space <name> --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 <s>] [--server <url>] [--creds <path>] [--on <instance>]
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 (`<package>@<version>`) 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 <package>` 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@<version>` 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 <name>` 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@<version>` 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 <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 <path>` 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 <s> --name <n> [--role <r>] [--channel <c>]
cotal join --link <url> | --token <t>
```
| Flag | Default | Meaning |
|---|---|---|
| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which mesh, and which credential |
| `--name <n>` | none | Your presence name |
| `--role <r>` | none | Your role |
| `--channel <c>` | link's first channel, else `general` | Channel to join; a static credential needs a read grant for it |
| `--kind <k>` | `agent` | Endpoint kind |
| `--link <url>` | none | Join link (`cotal://…`) |
| `--token <t>` | none | Join token |
| `--lifecycle-uid <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 <seconds>`, `--expires-at <unix-seconds>` | 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 <name> --provision --allow-subscribe general --allow-publish general --out <file>
cotal join --space <s> --creds <file> --lifecycle-uid <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 <npm-package>
cotal ext remove <name>
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 <name>`, 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 <package>`, 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 <original-spec>`
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 <pid>, then retry`, `delete <pidPath>, 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 <your-package>` 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 <checkout>/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 [<page>] [--refresh] [--json | --plain]
cotal docs --search <query> [--json | --plain]
```
| Flag | Default | Meaning |
|---|---|---|
| `--search <query>` | 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
`<cli-processes.mdx#daemon>` 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 <name>] [-v | --verbose] [--json | --plain]
cotal config get <key> [--user | --space <name>] [--json | --plain]
cotal config set <key> <value> [--user | --space <name>] [--json | --plain]
cotal config unset <key> [--user | --space <name>] [--json | --plain]
```
| Flag | Default | Meaning |
|---|---|---|
| `--user` | your defaults for a write | Your own values, with no space's values |
| `--space <name>` | 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 <bash|zsh|fish|powershell>
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 <file>`, `up -f <file>`, `--channels <path>`,
`--creds <path>`, `run start --file <program>` or `auth mint --out <private-file>` completes from the
shell's own file names, and so does a folder such as `space add --root <dir>`.
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 <shell>` 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 "<summary>" [--type <type>] [--email <contact>] [--details <text>] [--json|--plain]
cotal feedback "<summary>" --key-file <private-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.