Cotal
DocsAPI Reference
Workstation

CLI installation

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

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
FlagDefaultMeaning
--destinationaskedlocal: 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, -HaskedThe 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.
--runtimeasked; none in a scriptA runtime to set up besides the built-in pty: tmux, cmux, orca, herdr or an installed third-party one. Repeat it for each.
--projectaskedThe project folder: the space's root, whose .cotal/agents holds its personas. A script's folder must exist; a terminal offers to create it.
--spaceaskedThe space to prepare (local) or the registered space to use (self-hosted).
--connection-filenoneSelf-hosted: register the space from a connection bundle exported where it runs, with the checks space add --file makes.
--configure-onlyonSave preparation and start nothing. Refused with --start.
--startoffAfter 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.
--onnoneSelect 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, -doffWith --start, leave the managed agent running without opening a terminal view. Required in scripts, JSON and plain mode.
--demooffAlso seed the guided expert team (david, sven, me)
--fulloffThe guided flow with the guided team (implies --demo)
--yes, -yoffAsk nothing: every input comes from the flags
--skillsoffReconcile 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:

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). 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.

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 and, for maintainers, setup internals.

update

cotal update [--self] [--space <s>] [--server <url>] [--creds <path>]
cotal update --check [-j|--json|--plain]
FlagDefaultMeaning
--selfoffIf a newer release exists, install that exact validated cotal-ai version globally and reconcile through the newly installed binary
--space, --server, --credsresolved meshSelect the running manager whose continuity state is reported
--checkoffRead version availability and recorded extensions without installation or restart; takes no mesh selector or --self
--json, -j, --plainoffChoose the finite check report; requires --check

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 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; a newer binary is an informational notice with cotal update --self as the next command, not an automatic install.

Manager continuity

Before disk reconciliation, update reads the selected running manager. 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: its manager runs under another install, so there is no custody on this machine to preserve, and 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 on this machine 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 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.

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

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

cotal join --space <s> --name <n> [--role <r>] [--channel <c>]
cotal join --link <url> | --token <t>
FlagDefaultMeaning
--space <s> / --server <url> / --creds <path>resolved meshWhich mesh, and which credential
--name <n>noneYour presence name
--role <r>noneYour role
--channel <c>noneChannel to join
--kind <k>agentEndpoint kind
--link <url>noneJoin link (cotal://…)
--token <t>noneJoin token
--lifecycle-uid <uid>noneRequired 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
--tlsoffConnect over TLS

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 and Identity and auth.

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 binds the inbox provisioned for its lifecycle, so the space's owner mints the credential with cotal auth mint <name> --provision and hands over the lifecycle UID that mint reports. A credential minted without --provision has no inbox there, and join refuses, naming that lifecycle and the mint that makes one.

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

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. 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. 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 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

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:

FlagMeaning
(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.
--repairRecover 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.
--resetDiscard 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.
--forceRe-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). 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

cotal docs [<page>] [--refresh] [--json | --plain]
cotal docs --search <query> [--json | --plain]
FlagDefaultMeaning
--search <query>noneShow the sections that best match a query, each naming its page
--refreshoffWith a page: also try the version-pinned copy on docs.cotal.ai
--json / -joffOne JSON document
--plainoffThe 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

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]
FlagDefaultMeaning
--useryour defaults for a writeYour own values, with no space's values
--space <name>the selected space for a readYour values for one space
--verbose / -voffBare config also shows the preferences file and its revision, which its JSON result always holds
--json / -joffOne JSON document
--plainoffThe same report without color

Your preferences: harness, persona, detach-key, color and editor. 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 selected with cotal use, or with nothing selected the space spawn resolves from this folder, which is this folder's 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).

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

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 user, or group- or other-writable is named once, with the chmod go-w that fixes it when one does. 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:

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 a group- or other-writable one with its mode and one chmod go-w <paths> that fixes them all. 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. No command silently repairs custom wiring. Install and uninstall support one finite JSON document or the plain report.

feedback

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.

telemetry

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 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

See the current command contract for this behavior.

Startup

See the current command contract for this behavior.

Runtimes

See the current command contract for this behavior.

Setup outcomes

See the current command contract for this behavior.

Reconciliation

See the current command contract for this behavior.

Provider loading

See the current command contract for this behavior.

Extension inventory

See the current command contract for this behavior.

Extension mutations

See the current command contract for this behavior.

On this page