@cotal-ai/pi 0.105.0 → 0.105.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +3 -3
- package/dist/standalone.js +3 -3
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -27456,10 +27456,10 @@ import { isConcreteChannel as isConcreteChannel3, channelInAllow as channelInAll
|
|
|
27456
27456
|
import { parsePrincipalKey as parsePrincipalKey2, peerLabel as peerLabel3 } from "@cotal-ai/core";
|
|
27457
27457
|
|
|
27458
27458
|
// ../connector-core/dist/docs-bundle.generated.js
|
|
27459
|
-
var DOCS_VERSION = "0.105.
|
|
27459
|
+
var DOCS_VERSION = "0.105.1";
|
|
27460
27460
|
function loadDocsBundle() {
|
|
27461
27461
|
return {
|
|
27462
|
-
"version": "0.105.
|
|
27462
|
+
"version": "0.105.1",
|
|
27463
27463
|
"generatedFrom": "docs/*.md + SPEC.md + spec/cotal-lang.md + spec/cotal.schema.json",
|
|
27464
27464
|
"pages": [
|
|
27465
27465
|
{
|
|
@@ -27572,7 +27572,7 @@ function loadDocsBundle() {
|
|
|
27572
27572
|
"kind": "Guide (informative)",
|
|
27573
27573
|
"category": "Guide",
|
|
27574
27574
|
"summary": "The Claude Code connector turns a real claude session into a Cotal mesh peer.",
|
|
27575
|
-
"body": "# Connect Claude\n\n> **Guide** (informative) \xB7 **For:** operators \xB7 **Prereqs:** [Quickstart](getting-started.md)\n\nThe Claude Code connector turns a real `claude` session into a Cotal mesh peer. A bundled\nplugin inside the session joins NATS, maps lifecycle hooks to presence, and exposes the\nmesh tools. Nothing wraps Claude; it is an ordinary session that happens to be on the\nmesh.\n\nThe shared mesh runtime (agent, `cotal_*` tools, hook relay) lives in\n[`@cotal-ai/connector-core`](../extensions/connector-core); this connector is the thin\nClaude-specific adapter over it. Its lifecycle hook imports the relay from the\n`@cotal-ai/connector-core/relay` subpath, so each hook process loads the relay and its environment\nreaders and none of the NATS client, zod or yaml. Siblings: [OpenCode](connect-opencode.md) (beta),\n[Codex](connect-codex.md) (beta), [Jcode](connect-jcode.md) (beta), [Hermes](connect-hermes.md)\n(alpha), [pi](connect-pi.md) (alpha); the [Connectors](connectors.md) matrix compares them\nfeature-by-feature.\n\n## Choose a connection path\n\nUse one connection path per conversation. The Desktop app's Code tab and its ordinary chat\nare different hosts.\n\n| Where you use Claude | Connection path | How the space is chosen |\n| --- | --- | --- |\n| A Code session started by Cotal | Keep the connection supplied by `cotal spawn` | The spawn's target space |\n| Your own terminal, IDE or Desktop Code session | Native Cotal plugin | The native connection's target; see [personal sessions](#sessions-cotal-did-not-launch) |\n| Claude web or ordinary Desktop chat | Hosted HTTPS connector | The space approved during connector authorization |\n| Your own Code session where you explicitly choose hosted access | Hosted HTTPS connector | The space approved during connector authorization |\n\nThe native plugin can deliver incoming messages through supported Claude Code hooks. Hosted access\ncannot start a turn in an idle conversation. A hosted conversation receives messages while it calls\nmesh tools, including `cotal_wait`. Adding the hosted connector does not add native wake support.\n\n### Connect through the hosted service\n\nFor Cotal Cloud, the connector URL is `https://cloud.cotal.ai/mcp`. Other deployments advertise their\nown URL. In Claude web or Desktop chat, open Settings > Connectors > Add custom connector and enter\nthe URL. If the connector is already listed or required by an installed plugin, connect that entry\nrather than adding a duplicate. Complete authorization in the browser and choose the intended space.\n\nIn a Code session you started yourself, you can instead register and authenticate the HTTP server\nwith Claude Code's CLI:\n\n```bash\nclaude mcp add --transport http --scope user cotal-cloud https://cloud.cotal.ai/mcp\nclaude mcp login cotal-cloud\n```\n\nAuthenticate the existing entry if it is already registered. This CLI configuration does not\ncomplete authentication for Claude Desktop's separate Connectors settings. Where the local Cotal\nplugin is also installed, tell the conversation to use the hosted `cotal-cloud` server explicitly.\nA Code session started by Cotal must keep its managed connection.\n\nAuthorization gives the client access to the connector. Joining a conversation to the mesh is a\nseparate step: ask Claude to call `cotal_connect` with a name, open the returned approval page in\nyour signed-in browser, inspect the space and permissions, and approve if they are right. A pending\nrequest has not joined. In Claude Code with the plugin's native hooks module, the decision reaches\nthat conversation on its own: the plugin's server follows the pending request and, once you decide,\ntells the conversation to finish the connection, or that you denied it or let it expire. In Claude\nDesktop chat, which runs no native module, ask Claude to retry the connection. Confirm the resulting\nconnection with `cotal_orientation` and check that its space is the one you intended.\n\nIf Claude displays a success notification but its connector settings still offer Connect or report\nunfinished setup, do not treat the notification as proof of a working connection. Reopen the\nconnector details and check whether its tools are available. If the conflict remains, report it as\nan incomplete connector setup; it is not evidence that the separate conversation approval succeeded.\nDo not move project or ancestor `.cotal` directories, change mesh credentials, or add `COTAL_SPACE`\nto repair hosted authentication. Hosted access does not select a mesh from those directories.\n\n## Set up\n\n> **Team and Enterprise accounts:** [Enable Claude Code channels](#enable-channels-for-your-organization)\n> before running unattended agents. Channels are blocked by default, so incoming messages cannot\n> wake an idle session until an organization Owner enables them.\n\n```bash\ncotal setup --configure-only # install the plugin and seed one agent without startup\ncotal up # brings up the mesh + delivery daemon + a detached manager\n```\n\n`cotal setup` installs the cotal plugin for your user account, so every Claude Code session on\nthis machine gets the `cotal_*` tools, in any project. That covers the terminal, the IDE extensions\nand the Desktop app's local Code sessions. Setup can use Desktop's bundled Code executable when no\nterminal `claude` command is installed. Desktop-only users should open a local Code session once so\nDesktop downloads that executable. Setup checks it through the connector's own readiness probe;\nmanaged `spawn` still requires the terminal `claude` command. On a mesh you run with `cotal up`,\nsessions join through `cotal spawn`. A session you open yourself can join only a remote signed-in\nmesh whose authority offers connection approval: run setup once, reopen Claude and ask it to\nconnect (see [personal sessions](#sessions-cotal-did-not-launch)). No MCP\nconfiguration edits or special launch flags are needed for this native plugin path. It also shares your own MCP servers with spawned sessions on its first run (see\n[Sharing your MCP servers](#sharing-your-mcp-servers)), and seeds one `default` persona; `cotal up` brings up the local stack so\n`cotal spawn --detach` / `cotal_spawn` work right away. Re-running either is idempotent.\nA repeat `cotal setup` and `cotal update` refresh the copy under `~/.cotal/claude-plugin` and update\nexisting Claude plugin registrations, including older cached installations. After an npm upgrade,\nthe first regular Cotal command also refreshes these copies before accepting the new generation. Setup,\nskills installation and refresh share a machine-local lock through the final plugin check, so simultaneous\ncommands cannot overwrite a generation while another command verifies it. Disabled installations\nstay disabled during an update. Claude Code can apply an update run in a project folder to an\nenclosing project that has its own installation, so the folder's local or project installation stays\nold. That installation does not stop the refresh or later commands: Cotal prints a warning naming the\nfolder and the `claude plugin uninstall` command to run there to remove it. A user-scope installation\nthat does not update is still an error. `cotal status` checks the resolved payload's manifest version and\nfile contents. A folder-backed plugin can retain older version metadata in Claude's installation\nrecords; that metadata does not override the files Claude loads. Running sessions keep the\ncode they loaded; reload plugins or reopen Claude to load the update.\nThe install mechanics and the invariants behind them are in\n[setup internals](setup-internals.md).\n\n`cotal setup` also installs Cotal's authored Agent Skills (`SKILL.md`, the agentskills.io format) for\ncoordinating agent teams (today `team-topology`), from one canonical source, on two channels:\n\n- **Claude Code** gets a second, skills-only plugin, `cotal-skills`, from the same `cotal-mesh`\n marketplace, at **user scope** (machine-wide). The Claude connector declares and implements this\n setup provider, including the marketplace assets and native plugin commands; the base CLI only passes\n the vendor-neutral Agent Skills directory. The plugin carries no code and no core dependency,\n and uninstalls on its own with `claude plugin uninstall cotal-skills --scope user`. Its plugin version\n is stamped from the running CLI release. `cotal update` refreshes an existing skills plugin without\n enabling it or installing a removed one; `cotal setup --skills` also installs missing skills. `cotal setup` installs it on first run and on repeat\n runs, so upgraders are not left behind. The same provider reports the plugin and skills plugin rows\n in `cotal status`, which point a stale or missing skills plugin at `cotal setup --skills`.\n- **Every other harness** (Codex, Cursor, OpenCode, Gemini CLI, Windsurf/Devin) reads the cross-vendor\n `~/.agents/skills/` directory convention, which has no remote index, so `cotal setup` **reconciles** it\n (and `cotal setup --skills` does only that):\n it installs/updates each Cotal skill, backs up a copy you have edited to `SKILL.md.bak` before\n replacing it, and removes a Cotal skill that is no longer shipped. Only skills Cotal owns are touched;\n your own or third-party skills there are left alone. `cotal status` reports whether the drop is current,\n stale, missing, or has a retired skill to reconcile, and names `cotal setup --skills` as the remedy. This is the working cross-vendor path.\n\nCotal also generates an [Agent Skills discovery index](https://cotal.ai/.well-known/agent-skills/index.json)\non cotal.ai, but that RFC is still a draft with no harness consuming it yet, so it is a forward bet,\nnot a channel to rely on today.\n\n## Enable channels for your organization\n\nFor claude.ai Team and Enterprise organizations, channels are blocked by default. An organization\nOwner must enable them in [Organization settings > Claude Code > Channels](https://claude.ai/admin-settings/claude-code),\nor set this organization-managed setting:\n\n```json\n{\n \"channelsEnabled\": true\n}\n```\n\nUsers cannot override this setting through local or project settings. Cotal's\n`--dangerously-load-development-channels` launch flag cannot override it either. Enable channels\nbefore launching the sessions that need them.\n\nWhen channels are blocked, Cotal's MCP tools and hook-based inbox delivery still work when the agent\ntakes a turn. An incoming message cannot wake an idle Claude Code session. A successful spawn or a\nconnected MCP server does not prove idle wake is available; check Claude's startup notice before\nleaving agents unattended.\n\nPersonal Pro and Max accounts without an organization do not need this admin opt-in. Sessions still\nneed to opt into their channel, which Cotal's managed launch does for its own server. See\n[Anthropic's channel controls](https://code.claude.com/docs/en/channels#enterprise-controls) for\norganization policy and other authentication modes.\n\n## Spawn a session\n\n```bash\ncotal spawn # foreground: your default agent, in this terminal\ncotal spawn dave --detach # supervised: the manager runs it in a PTY\n```\n\nA spawn resolves a persona from `.cotal/agents/<name>.md` ([agent files](agent-files.md));\n`--model`, `--variant`, `--cwd`, `--prompt`, ACL overrides, and `--share-tools` apply to\nboth forms ([run a mesh](run-a-mesh.md) has the full resolution rules). The session joins\nwith identity from its environment and auto-registers presence by the time it is\ninteractive.\n\nInside the session, the agent orients with one read-only tool, `cotal_orientation`: its\nidentity, the channels it reads and may post to, its capabilities, the tools available,\nwho's present, and unread counts. The full tool surface is the\n[MCP tool catalog](mcp-tools.md). In auth mode the team-supervision tools\n(`cotal_spawn` / `cotal_persona` / `cotal_personas`) are injected **only** for personas declaring\n`capabilities: [spawn]` (the same grant that opens the privileged control subject), so an\nagent's toolset matches its declared capabilities. `cotal_run` is gated separately by\n`run`; use `capabilities: [spawn, run]` for both. Fresh setup defaults include both.\nSee [workflow tool setup](workflows.md#from-an-agent-session) for a first run and missing-tool checks.\nClearing retained history is\noperator-only ([run a mesh](run-a-mesh.md)), never an agent tool.\n\n## How it binds\n\nClaude Code exposes four integration surfaces, and three of them collapse into a single\ndual-purpose MCP server:\n\n| Surface | Mechanism |\n|---|---|\n| Outbound, ambient | command lifecycle hooks (`node dist/hook.cjs`) relayed to the connector over its authenticated local control socket (presence, activity) |\n| Outbound, deliberate | MCP tools `cotal_send` / `cotal_dm` / `cotal_anycast` (+ `cotal_feedback`) |\n| Inbound, pull | MCP tool `cotal_inbox` (same server) |\n| Inbound, push | Channel nudge + hook drain (below) |\n\nThe manager launches the *real* `claude` (no wrapper):\n\n```\nclaude --mcp-config '{\"mcpServers\":{\"cotal\":{\u2026}}}' \\\n --dangerously-load-development-channels server:cotal \\\n --permission-mode bypassPermissions\n# env: COTAL_SPACE, COTAL_NAME, COTAL_ROLE, COTAL_CHANNEL=1, plus claude's documented auth vars\n```\n\n- **Tool permissions.** A managed seat has no human at its terminal, so it launches with\n `--permission-mode bypassPermissions`: a prompt under the default mode would stop its first mesh\n tool call until someone answered it, and nobody can. To get prompts back, pass\n `--opt permission-mode=default` (or another mode); the connector adds the default only when the\n launch options carry neither `permission-mode` nor `dangerously-skip-permissions`. Claude refuses\n the bypass mode as root and under a policy that disables it; such a host must pass a mode.\n\n- **Model auth.** Locally, `claude` still reads macOS Keychain / `~/.claude`. In a container or\n CI there is no Keychain, so the connector forwards the documented credential set:\n `CLAUDE_CODE_OAUTH_TOKEN` (from `claude setup-token`), `ANTHROPIC_API_KEY` /\n `ANTHROPIC_AUTH_TOKEN`, and the cloud-provider flags plus their credential vars. Host-session\n markers (`CLAUDE_CODE_CHILD_SESSION`, `CLAUDECODE`) stay out so a nested seat still saves a\n transcript. See [Deploy](deploy.md).\n- **Persona privacy.** The persona body is written to a private file and Claude receives only\n `--append-system-prompt-file <path>`. The body never appears in the spawned process argv. The\n carrier is a 0600 file inside a 0700 directory on POSIX, with equivalent owner-only ACL hardening\n on Windows. That is OS-user isolation: any process running as your user can read it while it\n exists. The manager or the foreground `cotal spawn` removes it, and the shared-server MCP config\n file, once it has proved the `claude` process gone. If the launcher is killed first, a watcher\n started beside `claude` removes them when `claude` exits.\n- **MCP servers.** A spawned agent loads Claude's native MCP sources alongside Cotal and declared\n sharing overrides. Explicit `--share-tools` selections and carried private homes keep strict\n isolation. Claude still applies its own server approvals, disabled settings and managed policy\n (see below).\n- **Lean MCP server.** Every session runs its own cotal MCP server, so Node starts it with\n `--optimize-for-size --max-semi-space-size=1` to shrink its memory footprint. Managed launches and\n the plugin's `.mcp.json` (personal plugin sessions) both pass these flags. `--lite-mode` and\n `--jitless` would save more, but they remove `WebAssembly`, which Node's built-in `fetch` needs,\n so WebAssembly stays enabled.\n- **Light hook.** Claude Code starts the plugin's hook as a new process for every tool call, prompt\n and stop. The hook imports `@cotal-ai/core` only through small subpaths such as\n `@cotal-ai/core/peer-label`, never the package root, which would bundle YAML and NATS into it.\n `pnpm smoke:claude-hook-weight` holds that.\n- **Installed plugin.** The plugin is installed once, at user scope (`claude plugin install\n cotal@cotal-mesh --scope user`), because its hooks bind only to an *installed* plugin. Earlier\n releases installed it per project, at local scope; the next `cotal setup` removes those installs and\n keeps the user-scope one. If Claude Code cannot change a project's settings, setup still removes\n the others, then fails naming that project and the `claude plugin uninstall` command to run there. A session Cotal launches loads the package's own copy with `--plugin-dir`,\n which replaces the installed one for that session, so it still runs one cotal server.\n The repo's `.claude-plugin/marketplace.json` lists the committed plugin tree under\n `claude-plugin/`, which each release regenerates with the built bundles, the skills and the\n release version, so an install from the repo or from a pinned commit runs without a build\n ([Release](release.md)). `cotal setup` (npx, no clone) materializes the same marketplace under\n `~/.cotal/claude-plugin/` from the installed CLI (each plugin dir is rebuilt from scratch and\n atomically replaced, never merged, so no stale file rides in). The\n `cotal-skills` plugin installs from that same marketplace at user scope (`claude plugin install\n cotal-skills@cotal-mesh --scope user`); its manifest and install behavior ship inside the Claude connector, and\n its version tracks the CLI release so updates land.\n- **Identity-gated.** A launched session joins under the identity in `COTAL_NAME`, `COTAL_LINK`\n or `COTAL_AGENT_FILE`. A plain `claude` with none of them joins only when its conversation calls\n `cotal_connect`, so your own sessions do not appear as stray peers. Until then its MCP\n server builds no mesh agent and opens no broker connection. See\n [Sessions Cotal did not launch](#sessions-cotal-did-not-launch).\n- **Hands-free.** The dev-channels flag prints a one-time confirm prompt. The runtime waits for the\n dialog title in normalized terminal text and presses Enter once when it appears, so startup speed\n does not affect a supervised launch. Claude skips that prompt when channels are off for the\n session, for example under a Team or Enterprise organization whose managed settings do not set\n `channelsEnabled: true`, and opens its input screen instead. The runtime then ends its watch\n without a key once the input screen's footer shows `(shift+tab to cycle)` or `? for shortcuts` on\n two screen reads in a row. That seat gets mesh messages through its hooks only, so a message does\n not wake it while it is idle. The PTY runtimes read the child's screen, and the tmux, cmux, Orca\n and Herdr runtimes read the pane's screen. If neither the prompt nor the input screen appears\n within 15 seconds, the seat ends with a bounded error naming the unmatched prompt instead of\n hanging silently or answering another dialog. The PTY runtime writes that error to the seat's output and\n stops the child as a graceful stop does, so a child that ignores SIGTERM is killed 3 seconds\n later. Confirmation ends before the stop begins, so later child output receives no startup\n response. The other runtimes write the error to the manager's log.\n- **Trusted directory.** An authorized supervised spawn accepts the selected workspace through\n Claude's native trust dialog, which Claude draws in one of two layouts. When a numbered list\n shows Yes first and selected, the runtime confirms it with Enter. When No comes first and is\n selected, the runtime moves to Yes and confirms only after seeing Yes selected. It never confirms\n while No is selected, sends each response once, and stops watching after the\n development-channel confirmation or on the input screen. A changed or unsupported dialog receives no guessed input\n and still fails within the startup bound. Claude saves trust with its own repository and linked\n worktree rules; Cotal does not rewrite the manager's configuration. Accepting trust enables the\n workspace's hooks and settings, which can select a tool permission mode when no launch flag\n overrides it. Cotal adds no tool-permission bypass flag. Review the checkout before authorizing\n a spawn there. A foreground `cotal spawn` keeps the trust dialog in your terminal.\n\nInbound mesh messages arrive in context as\n`<channel source=\"cotal\" from=\"bob\" kind=\"dm\" \u2026>\u2026</channel>`: each meta key a tag\nattribute the agent can read for routing.\n\nThe consuming connector holds a crash-safe claim for its broker target, space, principal and\nlifecycle on this OS user account. A second patched session on the same host refuses before\npulling that inbox, even when its launch files and control socket differ. Reconnect keeps the\nclaim. Hooks and non-consuming helper connections do not claim it. The control listener binds\nbefore the broker connection and never removes an existing socket path.\n\nThis is cooperative same-host containment. Other hosts, broker URL aliases and older clients\nare not fenced. Preserve the existing session before choosing a distinct actor for a new\nlaunch. An uncertain shutdown keeps custody until the consuming process exits.\n\n## Sessions Cotal did not launch\n\nA Claude Code session you start yourself, where the `cotal` plugin is installed, gets the tools the\nhosted connector offers: `cotal_connect`, `cotal_wait`, `cotal_disconnect` and the routed mesh tools.\nNothing joins until the conversation calls `cotal_connect` with a name. The plugin's server then runs\n`cotal app-identity provision` with the CLI that `cotal setup` recorded beside the plugin\n(`dist/cli.json`), so it does not need `cotal` on `PATH`. Each conversation gets an identity of its\nown once a person approves it. To choose a registered mesh for this conversation, pass `space`:\n\n```json\n{\"name\":\"my-session\",\"space\":\"cotal_cloud\"}\n```\n\nThis is the native plugin's `cotal_connect` argument. It takes precedence over the saved selection\nand the project folder, without changing either. `cotal space` lists this machine's registered\nspaces. A blank or unknown space is refused; it is never replaced by another mesh. With no `space`,\nthe plugin uses the mesh a bare `cotal spawn` from the session's directory would pick, following\n[the CLI's selection rules](cli.md#selecting-a-space). Do not rename `.cotal` directories or add\n`COTAL_SPACE` to choose a personal conversation's target.\n\nA connected conversation must call `cotal_disconnect` before choosing another space. If its request\nis still awaiting approval, choosing another space first finishes recovery and retirement of the\nprevious request, then asks for a new approval. An unconfirmed retirement blocks the switch and keeps\nthe old recovery record. A second host cannot open the same native session on another mesh while\nits original consumer is still alive.\n\nThe hosted HTTPS connector chooses its space during browser authorization. Its OAuth grant is\nbound to that space; a native `space` argument cannot retarget a hosted grant.\n\nA static-auth or open mesh has no page where a person approves a connection, so the plugin\nopens no identity there. A plugin that `cotal setup` did not install has no record, and\n`cotal_connect` names that remedy.\n\nPersonal native Code sessions can join a signed-in mesh whose authority offers connection approval.\nThe plugin binds the real native session and transcript before requesting an identity. Each\nconversation gets a separate actor. A new identity is issued only after a person approves it in the\nauthority's signed-in web page. A model's tool arguments, a client flag or a native tool approval\nnever approve it. The request first comes back pending with the page's address and its expiry, and\nnothing is issued yet. Connecting again sends the same request and secret: while it is pending it\nanswers the same page, and once approved it issues the identity. A denial or an expiry drops the\nrequest, and the next connection asks again. No login proof reaches the conversation or its tools.\n\nWhile a bound Code conversation's request is pending, the plugin's server replays that same request\nto the authority every ten seconds, serialized with the conversation's own `cotal_connect`, until\nthe authority records a decision, the request passes its expiry, or the session ends. The server\nissues nothing by itself: an approved replay receives the identity the authority issued for that\nrequest, and the server holds it, unclaimed and unconnected, for the conversation's own\n`cotal_connect` with the same name and the `approval_id` from its pending result. The decision\nreaches only the native session that asked, through its wake reader, as one fixed line: approved\n(naming the `approval_id` to finish with), denied, or expired. Nothing else is written that way, and\nthe native module admits no other shape. A session that ends before claiming an approved identity\nhas it retired; a request still pending when the session ends stays saved, and the CLI\nfinishes it before that conversation's next request. An `approval_id` that is not this\nconversation's own pending request is refused before any request is sent. Desktop chat has no\nnative reader, so there the person asks Claude to retry.\nA remote mesh must advertise conversation identity v1 and connection approval v1 on its pinned HTTPS\ndiscovery endpoint. An absent capability, a different origin or a changed registration is refused\nbefore authenticated issuance, for a request without a selection too. There is no immediate-grant\nfallback. A signed-in mesh whose authority this machine holds has no approval page, so it issues no\nnew identity either.\n\nEvery identity sends and receives direct messages and publishes its own activity on its own event\nchannel. These come with the identity and are not optional, so the approval page shows them before\nthe person decides. Native activity sharing includes the messages the person types, assistant\nreplies, and each tool call's name with its start and end, after activation. It is not only presence\nor status. Tool arguments and tool results are withheld from the event channel, as on every connector\n([Event plane](#event-plane)). While a tool waits for permission, presence names it with a short\ndetail such as its command, and every roster reader sees that. Reasoning is not shared by default,\nand earlier or pre-approval history is not shared. The space's activity read permissions determine\nwho can read the event channel. Decline the connection if you do not want this sharing.\nA request without a selection proposes nothing beyond these rights: no role,\ncapabilities or ordinary channel reads.\n\n`cotal_connect` in a native Code session also takes a selection: `channels` to read\n(`access: \"read\"`) or read and post to (`\"read-post\"`), a `role`, and `capabilities` (`spawn`,\n`run`). The selection is a proposal. The person may approve it, change it within their own current\naccess, or approve nothing beyond the identity's own permissions. The issued grant must match what\nthe person approved item for item, or the connection is refused. When the authority refuses an item\noutside your own grant before asking anyone, nothing is issued and the reply names every refused\nitem. There is no fallback to the event-only identity. Its own event channel stays the first post\nentry. A remote mesh must also advertise `conversationIdentitySelection` version 1, or a selection\nis refused before any request.\nEndpoint calls are not selectable through this connection approval contract, which accepts only\nan empty endpoint selection. These conversations do not offer `cotal_describe` or `cotal_invoke`.\n\nA connection is offered the tools its grant can use: `cotal_spawn`, `cotal_persona`,\n`cotal_personas` and `cotal_despawn` with `spawn`, `cotal_run` with `run`, and `cotal_reconnect`.\nIts manager calls exchange the conversation's own secret for a manager-call view, so they carry only\nits grant and name the manager instance when more than one could answer. A conversation is not a\nmanaged seat. `cotal_despawn` without a name refuses rather than stopping your app, and\n`cotal_disconnect` retires only that connection. No run turn is addressed to it, because a workflow\naddresses turns to agents it spawned, so `cotal_yield` is not offered. Persona text and model pins\ndo not change the conversation's own instructions or model.\n\nThe plugin's ordinary command hooks supply the session and transcript binding. Its native module\nuses that binding without intercepting `classic.*` events, which Claude's built-in security guard\nwithholds from user plugins. The guard stays enabled. If an organization disables command hooks or\nnative modules, its administrator must permit them before this connection path can work. Command\nhooks still report lifecycle status and activity; the native module owns incoming message delivery.\n\nA signed-in identity keeps its request and secret in private local recovery state. A lost create\nresponse retries the same request without rotating its secret or allocating another identity.\nRetiring a request that is still pending drops it, and no identity is issued for it. An identity\nissued before connection approval keeps working, and its recovery stays exact. Another plugin server reuses that identity only after the process that held it no longer exists.\n`cotal_disconnect`, session end and a signal request retirement of that lifecycle only. Other\nconversations stay connected. When the mesh reports retirement pending or does not confirm it, the\nconversation stays disconnected and keeps its saved record. Every tool then reports the same\nunfinished cleanup with the mesh's retirement phase. Calling `cotal_disconnect` again retries it at\nonce. `cotal_connect` retries it too, and connects with a new identity once the mesh confirms\nretirement. A restarted plugin server recovers the same way. After each connection, the plugin\nretires the same mesh's saved identities whose plugin server process no longer exists, such as one\nkilled before it could retire them. This runs in the background, so the connection does not wait\nfor it. A server process that still exists keeps its identity, and a retirement that does not finish\nstays saved for a later run. A mesh requiring events refuses a connection without a native\ntranscript source.\n\nThe gateway's MCP annotations distinguish read-only queries from updates. Orientation, connection\nstatus, the roster, the docs and channel information are read-only. Sending messages, connecting and joining\nchannels are additive. Inbox reads and waits consume messages; status, channel mode, leaving and\ndisconnecting can replace or remove state. These hints do not grant access or override Claude's\npermission settings or an organization's approval policy.\n\nA message for a connected conversation starts a turn through the plugin's\n[native hooks module](https://code.claude.com/docs/en/plugins/mods/overview). This works in supported\nClaude Code hosts, including local sessions in the Desktop app's Code tab. The module runs a local\nwake reader as a child of the same host that runs the MCP server. Each server uses its own socket\nin a private directory. Short messages arrive in full with their sender, message ID and reply\ncontext. Claude can answer these without calling `cotal_inbox`. The module uses Claude's\nplugin-sender attribution, never the person's own identity. While Claude is busy, delivery happens\nat a main-conversation tool boundary before the next request. A request already being sent has a\nfixed message snapshot. A later arrival waits for another tool boundary or starts its own attributed\nturn when the current turn ends; storing a note alone does not count as delivery to that request.\n\nEach inline batch carries at most eight whole messages. Each rendered message must fit within\n1,024 Unicode code points, and the encoded socket frame must fit within 4,096 characters. Larger\nmessages remain queued. A notification-only reader or an oversized message uses\n`Cotal: a new message for connection_id <id>. Read it with cotal_inbox.` and a bounded preview.\nA preview is incomplete; Claude reads its contents through the inbox. An inline batch also names\nhow many other messages remain pending.\n\nThe transcript shows a compact Cotal indicator. Expand it to inspect the delivery or preview.\nThis changes only the display. Presentation observes only prompt rows; in-turn appends use their\nreturned row ID, without observing protected notes or skill output.\n\nSocket writes and previews do not acknowledge messages. A complete inline batch is committed only\nafter the native host confirms admission to the bound conversation. An idle prompt also needs its\nstored prompt row; a busy append needs its returned row ID and complete stored text. An offer waits\nfor a busy conversation's next safe boundary while its reader remains connected. Admission after\nclaim has its own deadline. This confirms host handoff, not that\nClaude understood or completed the work. Delivery remains at least once after an unknown result.\nMessage IDs distinguish retries from new requests.\n\nA wake follows the rule `cotal_wait` follows and does not compete with an active wait. Confirmed\ninline delivery rearms the next wake without an inbox call. A replacement reader reoffers unread\nwork. An offer from a closed connection cannot suppress its replacement connection's messages.\nClaude's Stop and organization controls still apply. Refused delivery stays pending without\na retry loop; genuine person-origin input resumes it with a fresh offer. `cotal_connect` reports\nwhether the incoming reader is attached; attachment alone does not prove native admission.\nWhen native modules are unavailable or blocked, the conversation can listen with `cotal_wait`.\n\nClaude Desktop chat does not run a plugin's local server. On macOS and Windows, where Claude Desktop\nis installed, `cotal setup` adds a `cotal` server to Desktop's own config,\n`claude_desktop_config.json` (Settings > Developer > Edit Config), and keeps every other entry there.\nIt runs the same server with `--desktop`, by the absolute paths of the node that runs the CLI and of\nthe plugin copy under `~/.cotal/claude-plugin`. Quit and reopen Claude Desktop to load it. A repeat\n`cotal setup` and `cotal update` keep the entry current, `cotal status` reports it, and a config\nfile that is not valid JSON is refused and left as it was. Desktop reads that file when it starts\nand writes the copy it holds back while you use it, so an entry added while it runs is lost. On\nmacOS, setup and update therefore leave the file alone while Claude Desktop is running, finish the\nrest of their work, and say so. Quit Claude Desktop, then run `cotal setup` again from a terminal\noutside it; a terminal in its Code tab keeps Desktop running. On Windows they still write while\nDesktop runs, because the loss was not reproduced there. Setup can use Desktop's bundled Code\nexecutable when there is no standalone `claude` CLI. Its installed MCP and command hooks use the\nrecorded Node executable, so the app need not find Node on `PATH`. On Linux, setup writes nothing\nfor Desktop Chat.\n\nDesktop Chat supplies no native Code transcript binding, so this local server cannot join a\nsigned-in space, such as one on Cotal Cloud, and refuses before identity issuance. Desktop Chat joins\na signed-in space only through the hosted connector that the space's discovery document advertises\nas `cloudCapabilities.hostedConnector`, version 1 with an HTTPS URL. Add that URL in Claude under\nSettings > Connectors > Add custom connector. Setup for a signed-in space writes no Desktop entry and\nprints the URL instead. `cotal status` prints it on the Claude Desktop row when the selected space is\nsigned in, and the refusal names it. When the space advertises none, all three say so. The Desktop\nserver's instructions tell the model to use the hosted connector for a signed-in space. The hosted\nconnector is a separate connection path; it does not supply autonomous chat wake.\n\nDesktop's working directory is not your project, so its mesh is the `cotal use` mesh, else the only\none running. Desktop runs no monitor, so a Desktop conversation hears messages when it calls a tool,\nand keeps calling `cotal_wait` when you ask it to keep listening. A chat with no turn running cannot\nbe woken by any server: Claude's chat surfaces start a turn only from the person.\n\n## How messages reach the session\n\nDurable deliveries land in the connector's inbox from JetStream consumers\n([SPEC \xA78](../SPEC.md#8-nats--jetstream-binding)); live channel traffic can instead arrive\nthrough an at-most-once core subscription. A durable message sent while the agent is busy\nor offline waits on the stream. Two things move a message from inbox to model; one\ndelivers, the other only wakes:\n\n- **Hook drain (delivery).** `SessionStart` / `UserPromptSubmit` hooks read automatic inbox items and\n inject them as `additionalContext`. This is the single authoritative path: deterministic and works\n on any Claude Code build. Quiet ambient is excluded and stays buffered for `cotal_inbox`.\n A message is **acked only once the hook reply carrying it has cleared both legs of its journey**:\n the connector's control socket to the hook process (which gives up after 2s), and the hook\n process's own stdout to Claude Code (which it force-exits 1s after starting to write). The relay\n sends a receipt back down the control socket from that stdout write's callback, and only on a\n clean write (a runtime whose pipe has gone away fails it), and the connector treats that receipt,\n not its own socket write, as delivery. So a large injection killed mid-flush, or one written to a\n broken pipe, leaves the message un-acked and JetStream redelivers it. What this does *not* prove is\n that Claude Code read or applied the reply: a payload small enough to fit the pipe buffer is\n reported written the moment the kernel takes it. That residual is why the path errs toward\n at-least-once rather than treating a confirmed write as a confirmed read. Acking when\n the reply was merely *formatted* meant a lost reply was a lost message: it was already marked\n handled, so its own redelivery was silently acked on arrival.\n A hook whose handler throws still returns an empty reply so the session is never blocked, and\n that reply carries nothing, so it commits nothing: the batch it had started to surface stays\n un-acked and goes out on a later frame. A manager-launched seat pulls its run turns\n (`turn-pending`) when its manager's turn notice arrives, when it connects, and after a reconnect\n while it holds a turn. The manager repeats a notice until the seat pulls the new turn, so an idle\n seat sends nothing. A pull that fails because the manager is down or unanswered is\n retried once every 15 seconds until one succeeds; a refusal is logged and not retried. The seat\n also drops any `turn-pending` row that breaks\n the manager contract, such as one with no integer deadline, and says so once in its log. A reply\n with no `turns` array changes nothing: the seat keeps the turns it already holds.\n This errs toward **at-least-once**: if a reply lands but its confirmation does not, the batch is\n surfaced again and flagged as a possible repeat. A duplicate injection is noise; a buried DM stops\n the peer answering at all.\n- **Channel nudge (wake).** An arriving message fires a `notifications/claude/channel`\n event that wakes an *idle* session into a turn, so the drain runs *now* instead of at\n the next prompt. The nudge never acks anything. A nudge that the host rejects is retried with a\n bounded backoff while anything is still pending. For an idle session it is the only wake source,\n so dropping it means silence until someone types. When the channel becomes active, the connector\n first re-fires a focus mention remembered during startup, otherwise one buffered wake. A rejected\n push keeps its bounded retry, and JetStream redelivery remains the durable backstop for unacked\n inbox items. Neither a redelivery nor that retry repeats a nudge already pushed for that message,\n whether the message had its own nudge or was counted in a batch one, so a session held in a long\n tool call gets one nudge per message. Once a hook frame carries the message, or the push that\n announced it fails, its next redelivery nudges again, so a reply that never reached Claude Code\n still recovers. If the channel cannot run at all, delivery still waits for the next hook. Live-only\n traffic has no durable retry.\n\n**Two priority tiers.** A *directed* message (DM, anycast, or a channel message that\n`@mentions` us) always nudges. *Ambient* channel chatter does not nudge mid-turn; it\naccumulates, and the `Stop` \u2192 idle transition fires one batch nudge so the backlog drains\ntogether.\n\n**Constraints (accepted).** Channels are a Claude Code research preview (\u2265 v2.1.80;\npermission relay \u2265 v2.1.81): Anthropic auth only, admin-enabled on Team/Enterprise, and a\ncustom channel needs the `--dangerously-load-development-channels` launch flag. The hook\ndrain does not depend on any of that; the channel only adds \"wake me when idle.\"\n\nThe same channel also relays **tool-permission requests** onto the mesh, so a peer (a\nhuman at the CLI, a policy node) can approve or deny an agent's pending tool call through\nCotal rather than a per-terminal prompt.\n\n### Attention\n\nAn agent picks how aggressively peer traffic reaches it with\n`cotal_status({ attention })` (three modes, orthogonal to presence):\n\n| arrival | open (default) | dnd | focus |\n|---|---|---|---|\n| directed (dm / anycast) | wake + inject | wake + inject | wake + inject |\n| channel `@mention` | wake + inject | wake + inject | ack-drop; wake to *pull*; not injected |\n| ambient channel chatter | wake when idle; hold while working | never wakes; injects next turn | ack-drop; recall via `cotal_inbox` |\n\nPer-channel overrides refine this: **quiet** (delivered, never wakes; `@mention` still\nwakes) and **muted** (dropped on receive, mentions included; DMs/anycast unaffected), set\nwith `cotal_channel_mode` or as agent-file defaults (`quiet:` / `muted:`,\n[agent files](agent-files.md)). A per-channel override is the final word for that channel.\nQuiet ambient is pull-only: it never hitchhikes on a human prompt, DM, mention, or other\nconnector-driven turn. `cotal_inbox` explicitly surfaces and clears it. A quiet-channel\n`@mention` remains automatic and injects normally.\n\nA pull is bounded too, and clears only what it hands over. One `cotal_inbox` call carries at most a\nreceivable window (direct messages and role requests first, then channel traffic, replayed history\nlast); whatever does not fit stays buffered, is named in the reply, and comes back on the next call.\nA message too large for one whole response is delivered in parts: once no smaller mail is waiting,\neach call carries its next part, and it is cleared only after its last part goes out, because clearing\nwhat was not handed over is the loss this bound exists to stop.\nThat matters most on the path where it is easiest to lose mail: reconnecting brings a channel-history\nreplay with it, so the largest payload and the least expendable message arrive in the same read.\n\nThe local inbox is bounded. On pathological overflow it evicts pull-only items first, then other\nchannel traffic, and a direct message or role request only when the whole buffer is directed mail.\nIt never evicts a message the connector is handing to the session or a turn is still carrying.\nAn evicted channel item is acknowledged. An evicted direct message or role request never is, and\nthe broker redelivers it after the ack wait until a redelivery finds room. A direct message stays\npending on the session's DM durable, where `cotal deliver pending <name>` counts it. A role request\nstays on its role's shared queue, which that command does not read. A full inbox therefore delays\ndirected mail until the session drains it.\nIf the bounded live/durable classification guard also fills, the connector fails closed:\notherwise-normal ambient becomes pull-only until restart. Muted hard-drop and normal focus recall\nstill take precedence. Focus also keeps a bounded exclusion list so mode toggles cannot recall\nquiet/muted traffic; if that safety bound fills, recall skips the affected channel and reports it\nas incomplete rather than risk resurfacing excluded content. Recall cannot tell one message with an\nempty id from an identical one with another disposition, so in focus such a message is held in the\nlocal inbox as pull-only instead of being dropped, and a mention of it still wakes the agent. When\nthe session settles an id-less copy, it reads the chat stream's last sequence. Identical copies\narrive in stream order, and those reads can answer out of order, so the first read that can see the\ncopies binds them in arrival order, latest first, each to the latest unbound stream copy at or below\nthe lowest sequence read for it or any later identical copy.\nWhile the connection stays up, every stream copy at or below that sequence reached the session\nfirst, so a later identical copy sent during a reconnect gap is above it and stays unbound. A copy\nthat arrives while a read runs may not be in that read, so it binds nothing there and recall reads\nthe channel again, up to three times; if it still could be a stream copy in the last read, recall\nleaves that stream copy in the stream and reports the channel as incomplete. A settled copy a\ncomplete read cannot bind is behind the focus start or out of retention, and is forgotten. Recall\nhands back into the inbox only the stream copies nothing is bound to, such as one sent during a\nreconnect gap or one the inbox evicted on overflow, and `cotal_inbox` hands each over once. Overflow\nfrees only the evicted copy, held or quiet, and a copy no read has bound yet keeps its place in\narrival order, so an identical muted copy stays out of recall. When\nthe inbox is full, recall leaves them in the stream for a later call and reports the channel as\nincomplete. A history read that fails, or a channel with replay off, settles nothing and is reported\nas incomplete, and recall calls run one at a time. If the sequence read for a settled copy fails,\nor answers only after the connection dropped, recall skips that channel for the rest of the focus\nperiod and reports it as incomplete. Recall cannot tell a late copy of a message it handed back from\na new identical message, so every copy takes its own disposition: a new identical quiet mention is\nstill delivered automatically, and a late copy can surface a second time.\nA recalled message that already went out in part is read to its last part, even if an exclusion\nlands after its first part. One session reads its inbox one call at a time: a `cotal_inbox` call\nthat overlaps another waits for it to finish, so neither decides from a view the other has already\nmoved past.\nIf the separate hard-drop disposition guard fills, channel traffic is dropped for the rest of the\nsession rather than risk a late copy bypassing an earlier muted/focus decision; DMs and anycast are\nunaffected.\n\nAttention is **advisory UX, not a boundary**: any peer can wake a dnd/focus agent by\nnaming it, and `muted` means \"I opted out of receiving\", not \"the channel is blocked\";\nthe broker still authorizes and delivers. Focus's real effect is shrinking the\nuntrusted-ambient injection surface (only subject-authenticated dm/anycast auto-inject).\nIt resets to **open** on `SessionStart`, so a restarted agent never stays silently deaf.\nYour attention is mirrored into presence so peers can see it.\n\nWhatever does reach a turn is framed so a peer cannot write the frame. A line that begins at column\nzero is written by the connector; one message is one line plus indented continuations, with the\nsender inside a single bracket pair. A message body, a sender name and role, and a service or\nchannel label are all peer-controlled, so each passes through the same neutralization the\n`cotal_inbox` reply uses: no line break a splitter may honour and no bracket survives into a\nrendered attribution. This matters more for an injected block than for a reply, because the agent\ndid not ask for it and so never had the chance to distrust it.\n\n## Presence mapping\n\nThe connector wires a small subset of Claude Code hooks to presence states; presence is\ncoarse, and \"what it is doing\" rides on activity updates. Presence is **advisory**: a presence\npublish that fails (the endpoint mid-reconnect, say) is swallowed and never prevents the same hook\nfrom delivering messages or flushing held ones.\nA `SessionStart` during an open turn, including compaction, preserves the current `working` or\n`waiting` status until `Stop`, `StopFailure`, or `SessionEnd` closes the turn.\n\n| Hook | \u2192 state |\n|---|---|\n| `SessionStart` | `idle` only when no turn is open (join; surfaces the inbox; captures the live model into `meta.model` when no pin) |\n| `UserPromptSubmit` | `working` (turn starts; surfaces the inbox) |\n| `PreToolUse` | no change; records *what* is about to run, so a permission wait can name it |\n| `PostToolBatch` | `working` when the main agent's batch resolves after a prompt left the seat `waiting`, which clears its condition so the turn's `Stop` yields; a subagent's batch (it carries `agent_id`) or one relayed after the turn's terminal changes nothing |\n| `Notification` (`permission_prompt` / `agent_needs_input`) | `waiting` with condition `approval` / `input` (activity leads with the pending tool, e.g. `Bash: git push \u2026`) |\n| `Stop` / `StopFailure` | `idle` (turn done / died on an API error; flushes anything held while busy). `StopFailure` also relays Claude Code's native error value as `condition.source` and maps it to the closed condition vocabulary. On the [event plane](#event-plane) it closes the run with `RUN_ERROR`. |\n| `SessionEnd` | `offline` (graceful leave) |\n\nThe connector also leaves gracefully when its stdin closes. An MCP client closes it to end the\nsession, and a killed `claude` closes it with no `SessionEnd`, so a dead session drops off the\nroster instead of staying on it as a live peer.\n\n`StopFailure` maps `rate_limit` and `overloaded` directly; auth and credential failures to\n`auth`; account and billing failures to `billing`; `invalid_request` to `request`;\n`model_not_found` to `model`; `server_error` to `server`; `max_output_tokens` to `context`; and\n`unknown` to `failed`. The native value remains in `condition.source`.\n\nHooks are relayed over the connector's **authenticated** local control endpoint (per-user\nsocket + per-launch token, constant-time checked), so a local process that finds the path\nstill can't drive presence or stop the agent. Claude Code's own\n[hooks reference](https://code.claude.com/docs/en/hooks) lists every hook event. The hooks the\nplugin registers, with their matchers, are listed in the\n[adapter README](../extensions/connector-claude-code/README.md#claude-code-hooks).\n\n## Event plane\n\nA connected native session publishes a structured account of its activity: the messages a person\ntyped, assistant replies, run boundaries, and each tool call's start and end. Tool arguments and\nresults are withheld (below), and a turn a peer message started opens a run without republishing\nthat message. Reasoning is off by default. Programs can read these events directly. The launcher\nsets `COTAL_EVENTS` by default; pass `--no-events` to opt out on an unrestricted\nspace. A user-auth registration with `policy: { events: \"required\" }` carries `eventsRequired` in the\nprivate launch material, so the connector arms even without `COTAL_EVENTS`; `--no-events` is refused.\nA hand-driven user-mode session may carry the same decision as `COTAL_EVENTS_REQUIRED=1`. Its own\npublish grant must cover `events.<owner>.<actor>` or the connector refuses before joining. A personal\nnative Code connection to a signed-in mesh uses the same recorder and captures its transcript\nboundary after approved issuance, before joining. Earlier history and time spent waiting for\napproval are not published.\nIts native lifecycle drives presence and recording for that session only; a subagent or another\nconversation cannot end its main run. The event channel keeps the space's normal read permissions.\nA personal connection makes manager calls only with its own selected grant.\n\nIf the event plane stops, the seat keeps running and its mesh connection, on every connector and\non every space. The connector log records `AG-UI emitter stopped` with the reason and the delay\nbefore it re-establishes the plane: at once after a failure, then every second while the plane\nstays down, a fixed wait that neither grows nor is capped, so the plane is back within a second\nof the fault clearing. The re-establish\nis a fresh start: it reopens the write-ahead log, settles any frame it left pending, builds the\nmapper from the run the log has open, and reads on from the stored cursor, which is the same\nrecovery a connector restart runs. A failed publish or a failed log write is never retried on the\nemitter it failed on. A turn that ends while the plane is down keeps its terminal, and so does a\nturn whose terminal's own log write failed: once the re-establish has read on, the run the log had\nopen is closed with that terminal, a failure as a failure, rather than left open for the next\nprompt to close as a finish. That holds when the next prompt was already written before the plane\ncame back: the failure is published after the failed turn's own records and before the next run\nopens, in a frame of the failed run, since a run error names no run and the frame is its only\nattribution. Every turn that ends while the plane is down keeps its own terminal. A terminal belongs\nto the last turn the transcript had begun when its hook fired: the connector reports a turn's\nterminal once that turn's records are written and before the next turn's are, so the position the\ntranscript has reached at the hook names the turn, including a turn the connector opens no run for,\nsuch as a task notification, which takes its terminal and publishes nothing. A terminal repeated\nfor a turn that has one replaces nothing and is kept for no later turn. A line the connector cannot\nread, written past that position while the plane was down, does not hold the terminal: the\nre-establish reports the gap and places the terminal after the records before it. A record no read\nanswers, such as an OpenCode part still filling when its turn died, holds it no more: the terminal\nis placed after the records before it, and the turn end reads the session once rather than without end. The OpenCode source reads every record at or before the end it reported at a turn end as settled, so a part left filling by a failed turn does not stop the turns after it from publishing. A terminal reported while the first start is still waiting on the broker reaches the emitter before its first read, so a prompt written in that window does not close the failed turn as a finish. The same placement runs with\nthe plane up: a terminal reported while the flush before it is still reading, with the next prompt\nwritten before that read, closes its own turn in the place of the finish that prompt synthesizes. The seat used to exit on a space that requires events; for Claude Code the\nconnector is the MCP server, so that exit showed as the server disconnecting, and Claude Code\nrestarted it on the next tool call.\n\nA transcript line that is not a record does not stop the plane. Claude writes the transcript, and a\nwrite cut short by a full disk leaves part of one record glued to the next on a single line, which\nnever becomes readable. The connector steps over that line, keeps the records after it, and writes\n`skipped an unreadable transcript record at offset <offset> (<bytes> bytes)` with the parser's\nreason to its stderr.\n\nA new session includes its first run even when Claude writes a positional startup prompt before the\nconnector receives `SessionStart`. That from-zero read is keyed only to Claude's explicit\n`source: \"startup\"`; resumed, forked, cleared, and compacted sessions adopt at the transcript boundary\ncaptured at that adopt, before the mesh link connects, so nothing Claude appends while the connector\nis still starting up lands behind the cursor and is silently dropped. Crash recovery follows the\ncursor already stored in the event write-ahead log, regardless of the new process's startup label.\n\nClaude starts each hook in its own process, so a prompt or stop relay can reach Cotal before the\n`SessionStart` relay. The connector holds those event flushes and the terminal until `SessionStart`\nsupplies the source, then enqueues adopt, flush, and close in that order.\n\n`SessionStart` can also run before the connector process has bound its local control socket. The hook\nthe `SessionStart` relay retries only transient pre-connect listener errors, with capped backoff\ninside its existing two-second budget. Later hooks and permanent local faults still fail open\nimmediately. Once a socket has connected, a broken exchange is not retried: the connector may\nalready have handled the frame, so replaying it could apply one lifecycle event twice.\nThat retained `SessionStart` can itself arrive before Claude creates the transcript path. A genuinely\nnew startup waits up to five seconds for that file with capped backoff, and the same deadline bounds\none stalled file read; expiry fails loud instead of silently losing the first run. A forked session\ngets the same wait, because Claude copies the parent transcript into the fork's own file after the\nhook, and then adopts at the end of that copy. Resumed, cleared and compacted starts and recovered\ncursors still require their existing source at once.\n\nTool arguments (`TOOL_CALL_ARGS`) and tool results (`TOOL_CALL_RESULT`) are not republished\nonto this channel. The durable emitter drops those events before they are written to the\nwrite-ahead log, because this channel's read ACL is not the ACL the tool ran under. Content is\nmandatory on both kinds, so the event is suppressed rather than emptied or replaced with a\nplaceholder. Tool start and end still go out. A restart that finds a pending pre-fix frame\nstill carrying those kinds HALTS rather than republishing it.\n\nThe channel is **`events.<owner>.<actor>`**, named after the session's principal. What the actor\nhalf is depends on the mesh, and the difference matters when you go looking for it: on a static mesh\nit is a key the manager allocated, never the display name, so two live agents sharing a display name\ndo not share a stream; on a user-auth mesh it is the agent's own name, because that is what the\nledger row is keyed on. Spelled out again with both halves below. The launch grants publish rights\non that channel alone. A spawn\nthat asks for a *different* agent's event channel is refused at the door rather than granted, since\nthat channel is that session's event stream. The same rule runs on restart: a manager\nresume document that names another agent's event channel is refused rather than adopted, because the\nmanaged row is re-armed from that document and the credential is re-minted from the row.\n\nThe rule reads a **concrete** channel, two principal tokens and nothing else. A pattern such as\n`events.<owner>.>` is not an event channel to it and passes untouched, governed by ordinary ACL\nauthority: on a user mesh the delegation envelope, on a static mesh the spawning credential itself.\nThat is deliberate, because the pattern is the form an operator writes on purpose for an observer,\nand it is worth knowing rather than assuming the fence is total.\n\nTo let something else read a plane, grant it out of band. The refusal prints the command for the\nmesh it is running on, spelled out in full, and only that one.\n\nOn a **user-auth** mesh:\n\n```bash\ncotal actor grant <reader> --owner <owner> --scope '' --allow-subscribe 'events.<owner>.<actor>' --allow-publish ''\n```\n\nEvery field, deliberately. `actor grant` is an upsert of the whole row, so it refuses a grant that\nleaves off any of the three ACL flags. Only `--full` turns an omitted flag into the wide default\n(`>` read, `>` post, `spawn,role:default` scope), which is the opposite of what a scoped watcher is for.\n\nOn a **static** mesh there is no actor ledger for `actor grant` to write to, and the refusal says\nso; mint the reader instead:\n\n```bash\ncotal auth mint watcher --profile agent --allow-subscribe 'events.<owner>.<actor>' --provision\n```\n\nThe **agent** profile, not the observer one. `mint` reads `--allow-subscribe` only for that\nprofile, and refuses it anywhere else: `--profile observer --allow-subscribe <channel>` exits\nnon-zero and writes no creds file, because the observer profile carries a fixed read set over the\nwhole chat plane, which is the opposite of what a scoped watcher is for. The agent profile also prints the lifecycle uid the\nreader needs, since an authed consuming endpoint refuses to start without one.\n\nOn an **open** mesh there is nothing to grant: the mesh has no credentials and no ACLs, so any peer\nthat lists the channel reads it, and the refusal says so instead of naming a command. The\nown-channel rule still applies there, because a spawn is not the place to hand out a read on\nanother agent's tool inputs and outputs.\n\nTwo things a reader has to do that are not obvious, both on `CotalEndpoint`. It must pass the event\nchannel in `channels`: an endpoint reads the channels it lists, so one constructed without\nthe event channel joins nothing and the frames never arrive. And it reads history with `readHistory(channel)`, the delivery daemon's mediated read, not\n`channelHistory(channel)`: a scoped credential is denied the ad-hoc consumer the direct read\ncreates, by design. `cotal console` and the web console already do both.\n\nThe `<owner>.<actor>` pair is the session's principal. On a user-auth mesh the actor half is the\nagent's own name, so the channel is `events.<your-owner>.<agent-name>`. On a\nstatic mesh the owner half is the literal `local` and the actor is a key the manager allocated, so\nthe channel is `events.local.<key>`; the spawn reply carries that key as `id`. Note\nthat `cotal console` and the web console keep event channels out of their channel lists on purpose,\nsince a plane is a machine feed rather than a conversation; they draw the frames when you open the\nchannel by name.\n\nThe rule governs the manager's doors, which are the ones a caller other than you can reach. A\nforeground `cotal spawn` on your own machine mints from your own signing material, so it can still\ngrant any channel you name: that is the out-of-band grant, not a way around the rule.\n\n**Failed turns publish run errors.** Claude Code decides for itself\nwhether a turn finished or died and fires one of two hooks accordingly, so the connector relays that\ndecision rather than making one of its own: a turn that ended on an API error ends its run with\n`RUN_ERROR` carrying the fixed message `run failed` and no code. Neither the detail Claude Code\nreported nor its error kind is published there: both are upstream values that can echo your prompt or\ntool output, and the events channel has a different read ACL. The error kind still reaches presence\nas the agent's condition (`rate_limit`, `auth`, `billing` and the rest). A turn that ended normally still\nends with a run-finished event carrying no outcome, which says the turn ended and does not claim it\nsucceeded.\n\nEvents are written to a per-session write-ahead log before they are published, so a hook that fires\nafter a restart resumes at the cursor it left rather than replaying or skipping, and a run that was\nopen when the session stopped is closed rather than left dangling.\n\nA delayed initial connection or a broker reconnect pauses event publication without closing MCP.\nThe recorder waits for both the mesh binding and its transport; unpublished records stay in the\ntranscript until the connection recovers. Stopping the connector cancels that wait. A recording error,\nincluding a conflicting transcript or damaged event state, stops the publishing plane and not the\nsession: it is reported with the delay before the next re-establish, on every space, and the seat keeps\nits mesh connection. A second transcript path offered to a bound recorder is refused the same way, and\nthe first path stays bound.\n\nOne channel carries **every session of one agent**, because it is named after the principal and not\nafter the session. Alongside the per-session logs the connector keeps one small record per principal,\nholding the last sequence the broker assigned on that channel, so a new session continues the stream\nits predecessor left instead of starting again from nothing. Both live under the events state root\n(`COTAL_WORKSPACE_ROOT`), and neither is something you edit by hand.\n\nA **missing** record is not a fault: the connector rebuilds it from the session logs beside it,\nwhich is how an agent that was already running before this record existed keeps its stream. That\nrebuild stops if any one of those session logs is damaged. Unreadable, not valid JSON, and written\nfor a different principal all count, and so does a session directory or a log that is a link rather\nthan the real file the connector wrote, or a log that has more than one name. A tip taken from the\nrest would be too low, and it would stop publication later with nothing left to point at the cause.\nThe connector names the file instead, and the only way past it is the directory removal described\nbelow, under the same condition. A record that **disagrees with the broker** is a fault, and the\nconnector stops publishing and says why rather than guessing. A record that **moved while a session\nwas writing to it** is refused the same way: it means something else wrote the principal's record,\nand the connector reports which value it held and which the file holds rather than writing over the\nlater one. There is no command to clear it. The state is the principal's directory under the events\nroot, and clearing it by hand means removing that directory whole: the sequence, the cursor and the\nper-session logs only mean anything together, so removing part of it leaves a state the next start\nrefuses. Removing it is only half a remedy, and the half that comes first is the channel. The\ndirectory is where the agent's memory of the tip lives, not the tip itself, so on a channel that\nstill holds frames the next session opens expecting an empty one and stops on the same\ndisagreement, with the logs a tip could have been rebuilt from now gone. Purge the channel first,\nthen remove the directory.\n\nReading it: `cotal console` and the web console draw event frames directly. A frame carries no text\npart by design, so a surface that renders a message as flat text shows a marker instead of prose.\n\n**On a per-user-auth mesh, the default event plane needs the spawner's grant to cover the channel.** The event\nchannel is added to the child's publish set, and delegation only narrows: an agent may hand down\na subset of what it holds and no more. So a peer-initiated spawn is refused unless the\nspawning identity's own grant already covers the child's event channel. The refusal prints the\nexact `cotal actor grant` command that widens it. An operator launch, whose chain reaches an\nadmin-scoped or roster row, is unaffected. Passing `events: false` is the explicit opt-out.\n\nArming the event plane through a typed spawn request (`manager.spawn` with `events`, including\nthe CLI's `cotal spawn --detach --events`) is allowed on a user mesh when the child runs under\nthe caller's owner. This also satisfies a registration policy that requires the plane. A spawn\nunder another owner still needs the caller's admin tier. Without it, an explicit request or a\nrequired plane is refused before provisioning. A silent non-owner caller in a space without that\npolicy has the plane disarmed with a notice in a successful reply. Later provisioning can still\nrefuse the spawn. The child's own-channel rule and the ledger's delegation envelope still apply.\n\n## Resume a session\n\n`--resume <session-id>` pulls an existing Claude session, its context and transcript,\ninto the mesh. It **forks**: Claude mints a *new* session id from that transcript\n(`--resume <id> --fork-session`), so the meshed agent gets its own session and the\noriginal is untouched.\n\n- `cotal spawn --resume <id>` (foreground) is the primary surface: the transcript is on\n *your* machine, and errors are Claude's own stderr, inline.\n- `--detach --resume <id> --on <instance>` carries a session held on *your* machine to that\n manager instance, which may run on another host. The CLI finds the transcript under your\n Claude config (`~/.claude`, or `$CLAUDE_CONFIG_DIR`), sends it through a JetStream Object\n Store bucket only that instance reads, under a writer credential pinned to that one transcript,\n and prints `carried session <id> to <instance>:\n sha256:<hex>, <sent> of <size> bytes sent in <chunks> chunks`. Without `--agent`, the CLI first asks that\n instance which harness the persona resolves to, so a persona with `agent: claude` is carried even\n when your `COTAL_DEFAULT_AGENT` names another connector. A re-run of the same bytes\n sends nothing, and an interrupted carry continues where it stopped. The seat forks it in a\n private Claude home under the manager's `.cotal/seat-homes/`, which no other seat's Claude\n lists or finds, and which is removed when the seat stops. When Claude starts the fork, the seat\n records the SHA-256 of the transcript it read from its own project; the manager stops a seat\n whose record names other bytes than the carried ones, or that records none within the join\n timeout after it joins, an uncertain launch included, and otherwise shows that record as the\n seat's provenance. `cotal attach` to such a seat names its source after the seat\n name, as `(resumed from <host>:<id>)`. A remote manager receives a carry when its host issues it a\n transfer reader. On a user-auth mesh the CLI exchanges the operator's login for a one-object\n `transfer-writer` view, which needs scope `admin`.\n- A session name in place of an id is refused, listing each session on this host that carries\n that name with its id, SHA-256 and modification time. An id this host does not hold resolves\n against the **manager host's** `~/.claude`, as before.\n- A seat-private home holds no login. The manager host needs `CLAUDE_CODE_OAUTH_TOKEN` (from\n `claude setup-token`), `ANTHROPIC_AUTH_TOKEN`, or a cloud provider selection in its\n environment; `ANTHROPIC_API_KEY` alone is refused. The private home trusts the authorized\n launch directory without changing the manager's home. Claude must be 2.1.234 or later.\n- The manager waits for a real outcome: `\u2713 started` means the agent *joined the mesh*,\n `\u2717 exited on launch` carries Claude's last output and the seat's exit diagnostic, such as a\n startup confirmation that never appeared, and an uncertain launch (~30 s) is reported without\n tearing the agent down.\n- `--opt resume=` with no id passes a bare `--resume`, which opens Claude's session picker, and the\n agent joins the mesh once you pick a session. A managed launch with it is ready once Claude runs:\n the CLI says the agent is waiting for you at Claude's session picker, attaches you to it in a\n terminal, `-d` included, or otherwise prints the attach command, and exits 0. `--opt resume=<id>`\n passes `--resume <id>` and joins as any launch does.\n- Resume is an **operator surface only**, deliberately not exposed on MCP `cotal_spawn`\n (a mesh peer naming host-local transcripts would widen `spawn` into transcript\n disclosure). Claude, Hermes, Jcode and pi support it, each by forking; OpenCode and Codex fail\n loud ([Connectors](connectors.md)).\n- Needs a `claude` new enough for `--resume \u2026 --fork-session` (verified on 2.1.197).\n\n## Sharing your MCP servers\n\nA spawned session loads the MCP servers your normal Claude Code configuration enables on the\nlaunch host: user, local, project, plugin and connected-account sources. Claude handles discovery,\nprecedence, disabled servers, project approval and managed policy. Cotal adds its mesh server\nand any overrides declared in `connectors.claude.mcpServers` ([format](config.md)). It does not\ncopy native server definitions or credentials into the launch file or onto another machine.\nThe selected `CLAUDE_CONFIG_DIR` remains in effect for an ordinary local spawn.\n\nFirst-run setup still seeds eligible user-scope declarations into the Cotal config and preserves\nan existing list. That list is not the source of truth for native discovery. A same-named declared\noverride takes precedence; changing the native definition does not rewrite an override you kept.\nAn empty Cotal sharing map leaves native discovery enabled.\n\nUse `--share-tools tavily,figma` to load only those declared servers plus Cotal, or\n`--share-tools none` for Cotal alone. These explicit selections use `--strict-mcp-config`.\nCarried sessions also stay strict and keep their seat-private home; operator profiles and\nconnected-account credentials are not copied into them. Native managed policy still applies.\n\nThe child environment stays allowlisted. References in declared shared servers and the selected\nuser-owned MCP configuration forward their named variables. Repository and plugin files cannot\nadd ambient secret names merely by referencing them; supply such names through an explicit\nshared declaration or the existing `spawn.env` policy. Native files retain their own configured\nvalues. Documented native MCP switches, including the connected-account opt-out and tool-search\nchoice, keep their explicit values. Cotal does not forward the entire parent environment or force\ntool definitions to load upfront.\n\nSharing a credential grants it to the session, so enable only servers the session may use.\nEach enabled server may start a process per seat; an explicit sharing selection can reduce that\ncost. Native discovery does not change Cotal's mesh identity, hooks or channel wake path.\n\n### One Cotal path per session\n\nA spawned session is on its mesh through the local Cotal server. A connected-account source can\nalso give it a hosted Cotal connector, which would join the same session a second time under\nanother name. The plugin's `PreToolUse` hook refuses every `cotal_*` tool of any server other than\nthe local one in a session Cotal launched, with the reason in the refusal. It decides in the hook\nprocess, so the refusal holds when the control socket is down. A Claude Code session that Cotal did\nnot launch keeps the hosted connector. Where the plugin is installed, its local server's\ninstructions tell the model to use the local path unless you ask for the hosted one.\n\nIn such a session the plugin also keeps each hosted connection with the session that opened it. A\n`PostToolUse` hook on the hosted `cotal_connect` records the returned `connection_id` under the\nplugin's data directory (`CLAUDE_PLUGIN_DATA`), and `cotal_disconnect` releases it. A fork made with\n`--fork-session` inherits that id in its transcript but has a new session id, so its calls with the\nid are refused and it is told to call `cotal_connect`. A resumed session and its subagents keep the\nsession id and pass. An id the plugin never saw opened goes to the hosted connector, which checks\nthe account.\nThe guard runs only where the `cotal` plugin is installed. A Claude Code session with only the\nhosted connector has no hook, so nothing records its connections and its forks are not refused.\n\nA hosted connector cannot start a turn, so a hosted conversation hears a message only when it calls a\ntool. `cotal_wait` holds until a message that would wake the agent arrives and returns it, or returns\nwith none after the service's bound. The conversation calls it again to keep listening. Every other\nhosted tool adds a line to its result when messages are waiting. A conversation with no turn running\nis not woken.\n\n## Host sessions in one process\n\nA host serving many managed seats can run their MCP servers in one process with the subpath\n`@cotal-ai/connector-claude-code/mcp`, which loads with both `import` and `require`:\n\n```ts\nimport { serveClaudeSession } from \"@cotal-ai/connector-claude-code/mcp\";\n\nconst session = await serveClaudeSession({ env, input, output });\nawait session.close();\n```\n\nLoading the module only exports it; it serves its own stdio only when run, as `node dist/mcp.cjs`.\nThe subpath ships CommonJS declarations so TypeScript hosts can use either module format,\nincluding CommonJS consumers with Node16 or Node18 module settings.\nClaude Code pushes `claude/channel` only to a stdio server it launched, so each `claude` still\nlaunches a stdio command, such as a small relay that pipes its stdio to the host.\n\n- **Managed only.** `env` is a complete launch environment, copied when the call starts. It must\n carry a mesh identity and the full control pair, or the call rejects before the session joins the\n mesh. The presence environment label, bearer commands, carried-fork records and feedback contact\n lookup use that copy. Feedback's Git fallback reads the session's global config, including its\n includes, without reading the host repository config. Set `COTAL_FEEDBACK_EMAIL` or pass the\n feedback tool's `email` argument to choose another contact. Include the `PATH`, `HOME` and other OS\n variables subprocesses need; the host's environment is not merged into them. Subprocesses still\n use the host process's working directory. A session's card does not: its `meta.cwd` is the `cwd`\n its own `SessionStart` hook reports on startup, resume or fork, absent until then, and unchanged by\n clear or compact. It is advisory display data that every reader of the space's roster can see. A plain `claude` with no identity is served by `mcp.cjs` run directly. Sharing a process\n does not isolate untrusted tenants or their files.\n- **Streams.** `input` carries JSON-RPC from the client and `output` carries this session's replies\n and `claude/channel` pushes. The caller owns both streams. Input that has emitted `end` or is\n destroyed rejects startup. Buffered unread input remains eligible. When an active session's\n `input` emits `end` or `close`, the session closes, including after a relay connection resets.\n Teardown may pause `input` and removes the transport's error handler. A host that reuses the\n stream must resume it as needed and keep its own stream error handlers.\n- **`fatalBind`.** Off by default: a control socket that cannot be bound rejects the call and the\n host keeps its other sessions. Only a process that is the session turns it on, as `mcp.cjs` does.\n- **Teardown.** `close()` closes the control socket, the wake policy, the AG-UI emitter, the mesh\n endpoint and the MCP server, and is idempotent. It removes its input listeners without closing\n the caller's streams. The returned promise rejects if teardown fails. A startup failure closes\n what was started before the call rejects. A manager shutdown frame or input EOF or closure closes the\n session and then calls `onShutdown(code)` once. A teardown failure is logged and reported with\n code 1, without becoming an unhandled rejection in the host. Close waits for a pending broker\n dial to settle. A dial that fails after shutdown starts is not a teardown failure. The session\n releases local inbox custody only after its connections are proved closed, so the same lifecycle\n can reopen after a successful close. An active duplicate or an unproved close retains custody.\n\n## Feedback\n\n`cotal_feedback` works out of the box: without a key it posts to the public intake at\n`https://cotal.ai/v1/feedback` (needs a contact email: `COTAL_FEEDBACK_EMAIL`, then\n`git config user.email`, else the agent asks). Set `COTAL_FEEDBACK_KEY=fbk_<key>` in a\nbeta tester's environment to route to the keyed intake (`Authorization: Bearer`, identity\nderived from the key); `COTAL_FEEDBACK_URL` overrides either endpoint. The CLI can send\ntoo: `cotal feedback \"<summary>\" [--type bug]`. Each submission carries\n`origin: human | agent`, whether the tester asked, or the agent auto-reported a major\nissue.\n"
|
|
27575
|
+
"body": "# Connect Claude\n\n> **Guide** (informative) \xB7 **For:** operators \xB7 **Prereqs:** [Quickstart](getting-started.md)\n\nThe Claude Code connector turns a real `claude` session into a Cotal mesh peer. A bundled\nplugin inside the session joins NATS, maps lifecycle hooks to presence, and exposes the\nmesh tools. Nothing wraps Claude; it is an ordinary session that happens to be on the\nmesh.\n\nThe shared mesh runtime (agent, `cotal_*` tools, hook relay) lives in\n[`@cotal-ai/connector-core`](../extensions/connector-core); this connector is the thin\nClaude-specific adapter over it. Its lifecycle hook imports the relay from the\n`@cotal-ai/connector-core/relay` subpath, so each hook process loads the relay and its environment\nreaders and none of the NATS client, zod or yaml. Siblings: [OpenCode](connect-opencode.md) (beta),\n[Codex](connect-codex.md) (beta), [Jcode](connect-jcode.md) (beta), [Hermes](connect-hermes.md)\n(alpha), [pi](connect-pi.md) (alpha); the [Connectors](connectors.md) matrix compares them\nfeature-by-feature.\n\n## Choose a connection path\n\nUse one connection path per conversation. The Desktop app's Code tab and its ordinary chat\nare different hosts.\n\n| Where you use Claude | Connection path | How the space is chosen |\n| --- | --- | --- |\n| A Code session started by Cotal | Keep the connection supplied by `cotal spawn` | The spawn's target space |\n| Your own terminal, IDE or Desktop Code session | Native Cotal plugin | The native connection's target; see [personal sessions](#sessions-cotal-did-not-launch) |\n| Claude web or ordinary Desktop chat | Hosted HTTPS connector | The space approved during connector authorization |\n| Your own Code session where you explicitly choose hosted access | Hosted HTTPS connector | The space approved during connector authorization |\n\nThe native plugin can deliver incoming messages through supported Claude Code hooks. Hosted access\ncannot start a turn in an idle conversation. A hosted conversation receives messages while it calls\nmesh tools, including `cotal_wait`. Adding the hosted connector does not add native wake support.\n\n### Connect through the hosted service\n\nFor Cotal Cloud, the connector URL is `https://cloud.cotal.ai/mcp`. Other deployments advertise their\nown URL. In Claude web or Desktop chat, open Settings > Connectors > Add custom connector and enter\nthe URL. If the connector is already listed or required by an installed plugin, connect that entry\nrather than adding a duplicate. Complete authorization in the browser and choose the intended space.\n\nIn a Code session you started yourself, you can instead register and authenticate the HTTP server\nwith Claude Code's CLI:\n\n```bash\nclaude mcp add --transport http --scope user cotal-cloud https://cloud.cotal.ai/mcp\nclaude mcp login cotal-cloud\n```\n\nAuthenticate the existing entry if it is already registered. This CLI configuration does not\ncomplete authentication for Claude Desktop's separate Connectors settings. Where the local Cotal\nplugin is also installed, tell the conversation to use the hosted `cotal-cloud` server explicitly.\nA Code session started by Cotal must keep its managed connection.\n\nAuthorization gives the client access to the connector. Joining a conversation to the mesh is a\nseparate step: ask Claude to call `cotal_connect` with a name, open the returned approval page in\nyour signed-in browser, inspect the space and permissions, and approve if they are right. A pending\nrequest has not joined. In Claude Code with the plugin's native hooks module, the decision reaches\nthat conversation on its own: the plugin's server follows the pending request and, once you decide,\ntells the conversation to finish the connection, or that you denied it or let it expire. In Claude\nDesktop chat, which runs no native module, ask Claude to retry the connection. Confirm the resulting\nconnection with `cotal_orientation` and check that its space is the one you intended.\n\nIf Claude displays a success notification but its connector settings still offer Connect or report\nunfinished setup, do not treat the notification as proof of a working connection. Reopen the\nconnector details and check whether its tools are available. If the conflict remains, report it as\nan incomplete connector setup; it is not evidence that the separate conversation approval succeeded.\nDo not move project or ancestor `.cotal` directories, change mesh credentials, or add `COTAL_SPACE`\nto repair hosted authentication. Hosted access does not select a mesh from those directories.\n\n## Set up\n\n> **Team and Enterprise accounts:** [Enable Claude Code channels](#enable-channels-for-your-organization)\n> before running unattended agents. Channels are blocked by default, so incoming messages cannot\n> wake an idle session until an organization Owner enables them.\n\n```bash\ncotal setup --configure-only # install the plugin and seed one agent without startup\ncotal up # brings up the mesh + delivery daemon + a detached manager\n```\n\n`cotal setup` installs the cotal plugin for your user account, so every Claude Code session on\nthis machine gets the `cotal_*` tools, in any project. That covers the terminal, the IDE extensions\nand the Desktop app's local Code sessions. Setup can use Desktop's bundled Code executable when no\nterminal `claude` command is installed. Desktop-only users should open a local Code session once so\nDesktop downloads that executable. Setup checks it through the connector's own readiness probe;\nmanaged `spawn` still requires the terminal `claude` command. On a mesh you run with `cotal up`,\nsessions join through `cotal spawn`. A session you open yourself can join only a remote signed-in\nmesh whose authority offers connection approval: run setup once, reopen Claude and ask it to\nconnect (see [personal sessions](#sessions-cotal-did-not-launch)). No MCP\nconfiguration edits or special launch flags are needed for this native plugin path. It also shares your own MCP servers with spawned sessions on its first run (see\n[Sharing your MCP servers](#sharing-your-mcp-servers)), and seeds one `default` persona; `cotal up` brings up the local stack so\n`cotal spawn --detach` / `cotal_spawn` work right away. Re-running either is idempotent.\nA repeat `cotal setup` and `cotal update` refresh the copy under `~/.cotal/claude-plugin` and update\nexisting Claude plugin registrations, including older cached installations. After an npm upgrade,\nthe first regular Cotal command also refreshes these copies before accepting the new generation. Setup,\nskills installation and refresh share a machine-local lock through the final plugin check, so simultaneous\ncommands cannot overwrite a generation while another command verifies it. Disabled installations\nstay disabled during an update. Claude Code can apply an update run in a project folder to an\nenclosing project that has its own installation, so the folder's local or project installation stays\nold. That installation does not stop the refresh or later commands: Cotal prints a warning naming the\nfolder and the `claude plugin uninstall` command to run there to remove it. A user-scope installation\nthat does not update is still an error. `cotal status` checks the resolved payload's manifest version and\nfile contents. A folder-backed plugin can retain older version metadata in Claude's installation\nrecords; that metadata does not override the files Claude loads. Running sessions keep the\ncode they loaded; reload plugins or reopen Claude to load the update.\nThe install mechanics and the invariants behind them are in\n[setup internals](setup-internals.md).\n\n`cotal setup` also installs Cotal's authored Agent Skills (`SKILL.md`, the agentskills.io format) for\ncoordinating agent teams (today `team-topology`), from one canonical source, on two channels:\n\n- **Claude Code** gets a second, skills-only plugin, `cotal-skills`, from the same `cotal-mesh`\n marketplace, at **user scope** (machine-wide). The Claude connector declares and implements this\n setup provider, including the marketplace assets and native plugin commands; the base CLI only passes\n the vendor-neutral Agent Skills directory. The plugin carries no code and no core dependency,\n and uninstalls on its own with `claude plugin uninstall cotal-skills --scope user`. Its plugin version\n is stamped from the running CLI release. `cotal update` refreshes an existing skills plugin without\n enabling it or installing a removed one; `cotal setup --skills` also installs missing skills. `cotal setup` installs it on first run and on repeat\n runs, so upgraders are not left behind. The same provider reports the plugin and skills plugin rows\n in `cotal status`, which point a stale or missing skills plugin at `cotal setup --skills`.\n- **Every other harness** (Codex, Cursor, OpenCode, Gemini CLI, Windsurf/Devin) reads the cross-vendor\n `~/.agents/skills/` directory convention, which has no remote index, so `cotal setup` **reconciles** it\n (and `cotal setup --skills` does only that):\n it installs/updates each Cotal skill, backs up a copy you have edited to `SKILL.md.bak` before\n replacing it, and removes a Cotal skill that is no longer shipped. Only skills Cotal owns are touched;\n your own or third-party skills there are left alone. `cotal status` reports whether the drop is current,\n stale, missing, or has a retired skill to reconcile, and names `cotal setup --skills` as the remedy. This is the working cross-vendor path.\n\nCotal also generates an [Agent Skills discovery index](https://cotal.ai/.well-known/agent-skills/index.json)\non cotal.ai, but that RFC is still a draft with no harness consuming it yet, so it is a forward bet,\nnot a channel to rely on today.\n\n## Enable channels for your organization\n\nFor claude.ai Team and Enterprise organizations, channels are blocked by default. An organization\nOwner must enable them in [Organization settings > Claude Code > Channels](https://claude.ai/admin-settings/claude-code),\nor set this organization-managed setting:\n\n```json\n{\n \"channelsEnabled\": true\n}\n```\n\nUsers cannot override this setting through local or project settings. Cotal's\n`--dangerously-load-development-channels` launch flag cannot override it either. Enable channels\nbefore launching the sessions that need them.\n\nWhen channels are blocked, Cotal's MCP tools and hook-based inbox delivery still work when the agent\ntakes a turn. An incoming message cannot wake an idle Claude Code session. A successful spawn or a\nconnected MCP server does not prove idle wake is available; check Claude's startup notice before\nleaving agents unattended.\n\nPersonal Pro and Max accounts without an organization do not need this admin opt-in. Sessions still\nneed to opt into their channel, which Cotal's managed launch does for its own server. See\n[Anthropic's channel controls](https://code.claude.com/docs/en/channels#enterprise-controls) for\norganization policy and other authentication modes.\n\n## Spawn a session\n\n```bash\ncotal spawn # foreground: your default agent, in this terminal\ncotal spawn dave --detach # supervised: the manager runs it in a PTY\n```\n\nA spawn resolves a persona from `.cotal/agents/<name>.md` ([agent files](agent-files.md));\n`--model`, `--variant`, `--cwd`, `--prompt`, ACL overrides, and `--share-tools` apply to\nboth forms ([run a mesh](run-a-mesh.md) has the full resolution rules). The session joins\nwith identity from its environment and auto-registers presence by the time it is\ninteractive.\n\nInside the session, the agent orients with one read-only tool, `cotal_orientation`: its\nidentity, the channels it reads and may post to, its capabilities, the tools available,\nwho's present, and unread counts. The full tool surface is the\n[MCP tool catalog](mcp-tools.md). In auth mode the team-supervision tools\n(`cotal_spawn` / `cotal_persona` / `cotal_personas`) are injected **only** for personas declaring\n`capabilities: [spawn]` (the same grant that opens the privileged control subject), so an\nagent's toolset matches its declared capabilities. `cotal_run` is gated separately by\n`run`; use `capabilities: [spawn, run]` for both. Fresh setup defaults include both.\nSee [workflow tool setup](workflows.md#from-an-agent-session) for a first run and missing-tool checks.\nClearing retained history is\noperator-only ([run a mesh](run-a-mesh.md)), never an agent tool.\n\n## How it binds\n\nClaude Code exposes four integration surfaces, and three of them collapse into a single\ndual-purpose MCP server:\n\n| Surface | Mechanism |\n|---|---|\n| Outbound, ambient | command lifecycle hooks (`node dist/hook.cjs`) relayed to the connector over its authenticated local control socket (presence, activity) |\n| Outbound, deliberate | MCP tools `cotal_send` / `cotal_dm` / `cotal_anycast` (+ `cotal_feedback`) |\n| Inbound, pull | MCP tool `cotal_inbox` (same server) |\n| Inbound, push | Channel nudge + hook drain (below) |\n\nThe manager launches the *real* `claude` (no wrapper):\n\n```\nclaude --mcp-config '{\"mcpServers\":{\"cotal\":{\u2026}}}' \\\n --dangerously-load-development-channels server:cotal \\\n --permission-mode bypassPermissions\n# env: COTAL_SPACE, COTAL_NAME, COTAL_ROLE, COTAL_CHANNEL=1, plus claude's documented auth vars\n```\n\n- **Tool permissions.** A managed seat has no human at its terminal, so it launches with\n `--permission-mode bypassPermissions`: a prompt under the default mode would stop its first mesh\n tool call until someone answered it, and nobody can. To get prompts back, pass\n `--opt permission-mode=default` (or another mode); the connector adds the default only when the\n launch options carry neither `permission-mode` nor `dangerously-skip-permissions`. Claude refuses\n the bypass mode as root and under a policy that disables it; such a host must pass a mode.\n\n- **Model auth.** Locally, `claude` still reads macOS Keychain / `~/.claude`. In a container or\n CI there is no Keychain, so the connector forwards the documented credential set:\n `CLAUDE_CODE_OAUTH_TOKEN` (from `claude setup-token`), `ANTHROPIC_API_KEY` /\n `ANTHROPIC_AUTH_TOKEN`, and the cloud-provider flags plus their credential vars. Host-session\n markers (`CLAUDE_CODE_CHILD_SESSION`, `CLAUDECODE`) stay out so a nested seat still saves a\n transcript. See [Deploy](deploy.md).\n- **Persona privacy.** The persona body is written to a private file and Claude receives only\n `--append-system-prompt-file <path>`. The body never appears in the spawned process argv. The\n carrier is a 0600 file inside a 0700 directory on POSIX, with equivalent owner-only ACL hardening\n on Windows. That is OS-user isolation: any process running as your user can read it while it\n exists. The manager or the foreground `cotal spawn` removes it, and the shared-server MCP config\n file, once it has proved the `claude` process gone. If the launcher is killed first, a watcher\n started beside `claude` removes them when `claude` exits.\n- **MCP servers.** A spawned agent loads Claude's native MCP sources alongside Cotal and declared\n sharing overrides. Explicit `--share-tools` selections and carried private homes keep strict\n isolation. Claude still applies its own server approvals, disabled settings and managed policy\n (see below).\n- **Lean MCP server.** Every session runs its own cotal MCP server, so Node starts it with\n `--optimize-for-size --max-semi-space-size=1` to shrink its memory footprint. Managed launches and\n the plugin's `.mcp.json` (personal plugin sessions) both pass these flags. `--lite-mode` and\n `--jitless` would save more, but they remove `WebAssembly`, which Node's built-in `fetch` needs,\n so WebAssembly stays enabled.\n- **Light hook.** Claude Code starts the plugin's hook as a new process for every tool call, prompt\n and stop. The hook imports `@cotal-ai/core` only through small subpaths such as\n `@cotal-ai/core/peer-label`, never the package root, which would bundle YAML and NATS into it.\n `pnpm smoke:claude-hook-weight` holds that.\n- **Installed plugin.** The plugin is installed once, at user scope (`claude plugin install\n cotal@cotal-mesh --scope user`), because its hooks bind only to an *installed* plugin. Earlier\n releases installed it per project, at local scope; the next `cotal setup` removes those installs and\n keeps the user-scope one. If Claude Code cannot change a project's settings, setup still removes\n the others, then fails naming that project and the `claude plugin uninstall` command to run there. A session Cotal launches loads the package's own copy with `--plugin-dir`,\n which replaces the installed one for that session, so it still runs one cotal server.\n The repo's `.claude-plugin/marketplace.json` lists the committed plugin tree under\n `claude-plugin/`, which each release regenerates with the built bundles, the skills and the\n release version, so an install from the repo or from a pinned commit runs without a build\n ([Release](release.md)). `cotal setup` (npx, no clone) materializes the same marketplace under\n `~/.cotal/claude-plugin/` from the installed CLI (each plugin dir is rebuilt from scratch and\n atomically replaced, never merged, so no stale file rides in). The\n `cotal-skills` plugin installs from that same marketplace at user scope (`claude plugin install\n cotal-skills@cotal-mesh --scope user`); its manifest and install behavior ship inside the Claude connector, and\n its version tracks the CLI release so updates land.\n- **Identity-gated.** A launched session joins under the identity in `COTAL_NAME`, `COTAL_LINK`\n or `COTAL_AGENT_FILE`. A plain `claude` with none of them joins only when its conversation calls\n `cotal_connect`, so your own sessions do not appear as stray peers. Until then its MCP\n server builds no mesh agent and opens no broker connection. See\n [Sessions Cotal did not launch](#sessions-cotal-did-not-launch).\n- **Hands-free.** The dev-channels flag prints a one-time confirm prompt. The runtime waits for the\n dialog title in normalized terminal text and presses Enter once when it appears, so startup speed\n does not affect a supervised launch. Claude skips that prompt when channels are off for the\n session, for example under a Team or Enterprise organization whose managed settings do not set\n `channelsEnabled: true`, and opens its input screen instead. The runtime then ends its watch\n without a key once the input screen's footer shows `(shift+tab to cycle)` or `? for shortcuts` on\n two screen reads in a row. That seat gets mesh messages through its hooks only, so a message does\n not wake it while it is idle. The PTY runtimes read the child's screen, and the tmux, cmux, Orca\n and Herdr runtimes read the pane's screen. If neither the prompt nor the input screen appears\n within 15 seconds, the seat ends with a bounded error naming the unmatched prompt instead of\n hanging silently or answering another dialog. The PTY runtime writes that error to the seat's output and\n stops the child as a graceful stop does, so a child that ignores SIGTERM is killed 3 seconds\n later. Confirmation ends before the stop begins, so later child output receives no startup\n response. The other runtimes write the error to the manager's log.\n- **Trusted directory.** An authorized supervised spawn accepts the selected workspace through\n Claude's native trust dialog, which Claude draws in one of two layouts. When a numbered list\n shows Yes first and selected, the runtime confirms it with Enter. When No comes first and is\n selected, the runtime moves to Yes and confirms only after seeing Yes selected. It never confirms\n while No is selected, sends each response once, and stops watching after the\n development-channel confirmation or on the input screen. A changed or unsupported dialog receives no guessed input\n and still fails within the startup bound. Claude saves trust with its own repository and linked\n worktree rules; Cotal does not rewrite the manager's configuration. Accepting trust enables the\n workspace's hooks and settings, which can select a tool permission mode when no launch flag\n overrides it. Cotal adds no tool-permission bypass flag. Review the checkout before authorizing\n a spawn there. A foreground `cotal spawn` keeps the trust dialog in your terminal.\n\nInbound mesh messages arrive in context as\n`<channel source=\"cotal\" from=\"bob\" kind=\"dm\" \u2026>\u2026</channel>`: each meta key a tag\nattribute the agent can read for routing.\n\nThe consuming connector holds a crash-safe claim for its broker target, space, principal and\nlifecycle on this OS user account. A second patched session on the same host refuses before\npulling that inbox, even when its launch files and control socket differ. Reconnect keeps the\nclaim. Hooks and non-consuming helper connections do not claim it. The control listener binds\nbefore the broker connection and never removes an existing socket path.\n\nThis is cooperative same-host containment. Other hosts, broker URL aliases and older clients\nare not fenced. Preserve the existing session before choosing a distinct actor for a new\nlaunch. An uncertain shutdown keeps custody until the consuming process exits.\n\n## Sessions Cotal did not launch\n\nA Claude Code session you start yourself, where the `cotal` plugin is installed, gets the tools the\nhosted connector offers: `cotal_connect`, `cotal_wait`, `cotal_disconnect` and the routed mesh tools.\nNothing joins until the conversation calls `cotal_connect` with a name. The plugin's server then runs\n`cotal app-identity provision` with the CLI that `cotal setup` recorded beside the plugin\n(`dist/cli.json`), so it does not need `cotal` on `PATH`. Each conversation gets an identity of its\nown once a person approves it. To choose a registered mesh for this conversation, pass `space`:\n\n```json\n{\"name\":\"my-session\",\"space\":\"cotal_cloud\"}\n```\n\nThis is the native plugin's `cotal_connect` argument. It takes precedence over the saved selection\nand the project folder, without changing either. `cotal space` lists this machine's registered\nspaces. A blank or unknown space is refused; it is never replaced by another mesh. With no `space`,\nthe plugin uses the mesh a bare `cotal spawn` from the session's directory would pick, following\n[the CLI's selection rules](cli.md#selecting-a-space). Do not rename `.cotal` directories or add\n`COTAL_SPACE` to choose a personal conversation's target.\n\nA connected conversation must call `cotal_disconnect` before choosing another name, space or grant.\nBefore it connects, you can change a pending name or permissions by calling `cotal_connect` with the\nnew values and no `approval_id`. You do not need to approve the unwanted request or start another\nconversation. The old watcher stops, the saved request is recovered and retired, and the replacement\ngets a fresh approval. Changing the space follows the same cleanup rule. If approval raced the\nchange, its issued identity is retired before the replacement is requested; its grant is never\nreused. An unconfirmed retirement blocks the switch and keeps the old recovery record. A second\nhost cannot replace a request while its original consumer is still alive.\n\nThe hosted HTTPS connector chooses its space during browser authorization. Its OAuth grant is\nbound to that space; a native `space` argument cannot retarget a hosted grant.\n\nA static-auth or open mesh has no page where a person approves a connection, so the plugin\nopens no identity there. A plugin that `cotal setup` did not install has no record, and\n`cotal_connect` names that remedy.\n\nPersonal native Code sessions can join a signed-in mesh whose authority offers connection approval.\nThe plugin binds the real native session and transcript before requesting an identity. Each\nconversation gets a separate actor. A new identity is issued only after a person approves it in the\nauthority's signed-in web page. A model's tool arguments, a client flag or a native tool approval\nnever approve it. The request first comes back pending with the page's address and its expiry, and\nnothing is issued yet. Connecting again sends the same request and secret: while it is pending it\nanswers the same page, and once approved it issues the identity. A denial or an expiry drops the\nrequest, and the next connection asks again. No login proof reaches the conversation or its tools.\n\nWhile a bound Code conversation's request is pending, the plugin's server replays that same request\nto the authority every ten seconds, serialized with the conversation's own `cotal_connect`, until\nthe authority records a decision, the request passes its expiry, or the session ends. The server\nissues nothing by itself: an approved replay receives the identity the authority issued for that\nrequest, and the server holds it, unclaimed and unconnected, for the conversation's own\n`cotal_connect` with the same name and the `approval_id` from its pending result. The decision\nreaches only the native session that asked, through its wake reader, as one fixed line: approved\n(naming the `approval_id` to finish with), denied, or expired. Nothing else is written that way, and\nthe native module admits no other shape. A session that ends before claiming an approved identity\nhas it retired; a request still pending when the session ends stays saved, and the CLI\nfinishes it before that conversation's next request. An `approval_id` that is not this\nconversation's own pending request is refused before any request is sent. Desktop chat has no\nnative reader, so there the person asks Claude to retry.\nA remote mesh must advertise conversation identity v1 and connection approval v1 on its pinned HTTPS\ndiscovery endpoint. An absent capability, a different origin or a changed registration is refused\nbefore authenticated issuance, for a request without a selection too. There is no immediate-grant\nfallback. A signed-in mesh whose authority this machine holds has no approval page, so it issues no\nnew identity either.\n\nEvery identity sends and receives direct messages and publishes its own activity on its own event\nchannel. These come with the identity and are not optional, so the approval page shows them before\nthe person decides. Native activity sharing includes the messages the person types, assistant\nreplies, and each tool call's name with its start and end, after activation. It is not only presence\nor status. Tool arguments and tool results are withheld from the event channel, as on every connector\n([Event plane](#event-plane)). While a tool waits for permission, presence names it with a short\ndetail such as its command, and every roster reader sees that. Reasoning is not shared by default,\nand earlier or pre-approval history is not shared. The space's activity read permissions determine\nwho can read the event channel. Decline the connection if you do not want this sharing.\nA request without a selection proposes nothing beyond these rights: no role,\ncapabilities or ordinary channel reads.\n\n`cotal_connect` in a native Code session also takes a selection: `channels` to read\n(`access: \"read\"`) or read and post to (`\"read-post\"`), a `role`, and `capabilities` (`spawn`,\n`run`). The selection is a proposal. The person may approve it, change it within their own current\naccess, or approve nothing beyond the identity's own permissions. The issued grant must match what\nthe person approved item for item, or the connection is refused. When the authority refuses an item\noutside your own grant before asking anyone, nothing is issued and the reply names every refused\nitem. There is no fallback to the event-only identity. Its own event channel stays the first post\nentry. A remote mesh must also advertise `conversationIdentitySelection` version 1, or a selection\nis refused before any request.\nEndpoint calls are not selectable through this connection approval contract, which accepts only\nan empty endpoint selection. These conversations do not offer `cotal_describe` or `cotal_invoke`.\n\nA connection is offered the tools its grant can use: `cotal_spawn`, `cotal_persona`,\n`cotal_personas` and `cotal_despawn` with `spawn`, `cotal_run` with `run`, and `cotal_reconnect`.\nIts manager calls exchange the conversation's own secret for a manager-call view, so they carry only\nits grant and name the manager instance when more than one could answer. A conversation is not a\nmanaged seat. `cotal_despawn` without a name refuses rather than stopping your app, and\n`cotal_disconnect` retires only that connection. No run turn is addressed to it, because a workflow\naddresses turns to agents it spawned, so `cotal_yield` is not offered. Persona text and model pins\ndo not change the conversation's own instructions or model.\n\nThe plugin's ordinary command hooks supply the session and transcript binding. Its native module\nuses that binding without intercepting `classic.*` events, which Claude's built-in security guard\nwithholds from user plugins. The guard stays enabled. If an organization disables command hooks or\nnative modules, its administrator must permit them before this connection path can work. Command\nhooks still report lifecycle status and activity; the native module owns incoming message delivery.\n\nA signed-in identity keeps its request and secret in private local recovery state. A lost create\nresponse retries the same request without rotating its secret or allocating another identity.\nRetiring a request that is still pending drops it, and no identity is issued for it. An identity\nissued before connection approval keeps working, and its recovery stays exact. Another plugin server reuses that identity only after the process that held it no longer exists.\n`cotal_disconnect`, session end and a signal request retirement of that lifecycle only. Other\nconversations stay connected. When the mesh reports retirement pending or does not confirm it, the\nconversation stays disconnected and keeps its saved record. Every tool then reports the same\nunfinished cleanup with the mesh's retirement phase. Calling `cotal_disconnect` again retries it at\nonce. `cotal_connect` retries it too, and connects with a new identity once the mesh confirms\nretirement. A restarted plugin server recovers the same way. After each connection, the plugin\nretires the same mesh's saved identities whose plugin server process no longer exists, such as one\nkilled before it could retire them. This runs in the background, so the connection does not wait\nfor it. A server process that still exists keeps its identity, and a retirement that does not finish\nstays saved for a later run. A mesh requiring events refuses a connection without a native\ntranscript source.\n\nThe gateway's MCP annotations distinguish read-only queries from updates. Orientation, connection\nstatus, the roster, the docs and channel information are read-only. Sending messages, connecting and joining\nchannels are additive. Inbox reads and waits consume messages; status, channel mode, leaving and\ndisconnecting can replace or remove state. These hints do not grant access or override Claude's\npermission settings or an organization's approval policy.\n\nA message for a connected conversation starts a turn through the plugin's\n[native hooks module](https://code.claude.com/docs/en/plugins/mods/overview). This works in supported\nClaude Code hosts, including local sessions in the Desktop app's Code tab. The module runs a local\nwake reader as a child of the same host that runs the MCP server. Each server uses its own socket\nin a private directory. Short messages arrive in full with their sender, message ID and reply\ncontext. Claude can answer these without calling `cotal_inbox`. The module uses Claude's\nplugin-sender attribution, never the person's own identity. While Claude is busy, delivery happens\nat a main-conversation tool boundary before the next request. A request already being sent has a\nfixed message snapshot. A later arrival waits for another tool boundary or starts its own attributed\nturn when the current turn ends; storing a note alone does not count as delivery to that request.\n\nEach inline batch carries at most eight whole messages. Each rendered message must fit within\n1,024 Unicode code points, and the encoded socket frame must fit within 4,096 characters. Larger\nmessages remain queued. A notification-only reader or an oversized message uses\n`Cotal: a new message for connection_id <id>. Read it with cotal_inbox.` and a bounded preview.\nA preview is incomplete; Claude reads its contents through the inbox. An inline batch also names\nhow many other messages remain pending.\n\nThe transcript shows a compact Cotal indicator. Expand it to inspect the delivery or preview.\nThis changes only the display. Presentation observes only prompt rows; in-turn appends use their\nreturned row ID, without observing protected notes or skill output.\n\nSocket writes and previews do not acknowledge messages. A complete inline batch is committed only\nafter the native host confirms admission to the bound conversation. An idle prompt also needs its\nstored prompt row; a busy append needs its returned row ID and complete stored text. An offer waits\nfor a busy conversation's next safe boundary while its reader remains connected. Admission after\nclaim has its own deadline. This confirms host handoff, not that\nClaude understood or completed the work. Delivery remains at least once after an unknown result.\nMessage IDs distinguish retries from new requests.\n\nA wake follows the rule `cotal_wait` follows and does not compete with an active wait. Confirmed\ninline delivery rearms the next wake without an inbox call. A replacement reader reoffers unread\nwork. An offer from a closed connection cannot suppress its replacement connection's messages.\nClaude's Stop and organization controls still apply. Refused delivery stays pending without\na retry loop; genuine person-origin input resumes it with a fresh offer. `cotal_connect` reports\nwhether the incoming reader is attached; attachment alone does not prove native admission.\nWhen native modules are unavailable or blocked, the conversation can listen with `cotal_wait`.\n\nClaude Desktop chat does not run a plugin's local server. On macOS and Windows, where Claude Desktop\nis installed, `cotal setup` adds a `cotal` server to Desktop's own config,\n`claude_desktop_config.json` (Settings > Developer > Edit Config), and keeps every other entry there.\nIt runs the same server with `--desktop`, by the absolute paths of the node that runs the CLI and of\nthe plugin copy under `~/.cotal/claude-plugin`. Quit and reopen Claude Desktop to load it. A repeat\n`cotal setup` and `cotal update` keep the entry current, `cotal status` reports it, and a config\nfile that is not valid JSON is refused and left as it was. Desktop reads that file when it starts\nand writes the copy it holds back while you use it, so an entry added while it runs is lost. On\nmacOS, setup and update therefore leave the file alone while Claude Desktop is running, finish the\nrest of their work, and say so. Quit Claude Desktop, then run `cotal setup` again from a terminal\noutside it; a terminal in its Code tab keeps Desktop running. On Windows they still write while\nDesktop runs, because the loss was not reproduced there. Setup can use Desktop's bundled Code\nexecutable when there is no standalone `claude` CLI. Its installed MCP and command hooks use the\nrecorded Node executable, so the app need not find Node on `PATH`. On Linux, setup writes nothing\nfor Desktop Chat.\n\nDesktop Chat supplies no native Code transcript binding, so this local server cannot join a\nsigned-in space, such as one on Cotal Cloud, and refuses before identity issuance. Desktop Chat joins\na signed-in space only through the hosted connector that the space's discovery document advertises\nas `cloudCapabilities.hostedConnector`, version 1 with an HTTPS URL. Add that URL in Claude under\nSettings > Connectors > Add custom connector. Setup for a signed-in space writes no Desktop entry and\nprints the URL instead. `cotal status` prints it on the Claude Desktop row when the selected space is\nsigned in, and the refusal names it. When the space advertises none, all three say so. The Desktop\nserver's instructions tell the model to use the hosted connector for a signed-in space. The hosted\nconnector is a separate connection path; it does not supply autonomous chat wake.\n\nDesktop's working directory is not your project, so its mesh is the `cotal use` mesh, else the only\none running. Desktop runs no monitor, so a Desktop conversation hears messages when it calls a tool,\nand keeps calling `cotal_wait` when you ask it to keep listening. A chat with no turn running cannot\nbe woken by any server: Claude's chat surfaces start a turn only from the person.\n\n## How messages reach the session\n\nDurable deliveries land in the connector's inbox from JetStream consumers\n([SPEC \xA78](../SPEC.md#8-nats--jetstream-binding)); live channel traffic can instead arrive\nthrough an at-most-once core subscription. A durable message sent while the agent is busy\nor offline waits on the stream. Two things move a message from inbox to model; one\ndelivers, the other only wakes:\n\n- **Hook drain (delivery).** `SessionStart` / `UserPromptSubmit` hooks read automatic inbox items and\n inject them as `additionalContext`. This is the single authoritative path: deterministic and works\n on any Claude Code build. Quiet ambient is excluded and stays buffered for `cotal_inbox`.\n A message is **acked only once the hook reply carrying it has cleared both legs of its journey**:\n the connector's control socket to the hook process (which gives up after 2s), and the hook\n process's own stdout to Claude Code (which it force-exits 1s after starting to write). The relay\n sends a receipt back down the control socket from that stdout write's callback, and only on a\n clean write (a runtime whose pipe has gone away fails it), and the connector treats that receipt,\n not its own socket write, as delivery. So a large injection killed mid-flush, or one written to a\n broken pipe, leaves the message un-acked and JetStream redelivers it. What this does *not* prove is\n that Claude Code read or applied the reply: a payload small enough to fit the pipe buffer is\n reported written the moment the kernel takes it. That residual is why the path errs toward\n at-least-once rather than treating a confirmed write as a confirmed read. Acking when\n the reply was merely *formatted* meant a lost reply was a lost message: it was already marked\n handled, so its own redelivery was silently acked on arrival.\n A hook whose handler throws still returns an empty reply so the session is never blocked, and\n that reply carries nothing, so it commits nothing: the batch it had started to surface stays\n un-acked and goes out on a later frame. A manager-launched seat pulls its run turns\n (`turn-pending`) when its manager's turn notice arrives, when it connects, and after a reconnect\n while it holds a turn. The manager repeats a notice until the seat pulls the new turn, so an idle\n seat sends nothing. A pull that fails because the manager is down or unanswered is\n retried once every 15 seconds until one succeeds; a refusal is logged and not retried. The seat\n also drops any `turn-pending` row that breaks\n the manager contract, such as one with no integer deadline, and says so once in its log. A reply\n with no `turns` array changes nothing: the seat keeps the turns it already holds.\n This errs toward **at-least-once**: if a reply lands but its confirmation does not, the batch is\n surfaced again and flagged as a possible repeat. A duplicate injection is noise; a buried DM stops\n the peer answering at all.\n- **Channel nudge (wake).** An arriving message fires a `notifications/claude/channel`\n event that wakes an *idle* session into a turn, so the drain runs *now* instead of at\n the next prompt. The nudge never acks anything. A nudge that the host rejects is retried with a\n bounded backoff while anything is still pending. For an idle session it is the only wake source,\n so dropping it means silence until someone types. When the channel becomes active, the connector\n first re-fires a focus mention remembered during startup, otherwise one buffered wake. A rejected\n push keeps its bounded retry, and JetStream redelivery remains the durable backstop for unacked\n inbox items. Neither a redelivery nor that retry repeats a nudge already pushed for that message,\n whether the message had its own nudge or was counted in a batch one, so a session held in a long\n tool call gets one nudge per message. Once a hook frame carries the message, or the push that\n announced it fails, its next redelivery nudges again, so a reply that never reached Claude Code\n still recovers. If the channel cannot run at all, delivery still waits for the next hook. Live-only\n traffic has no durable retry.\n\n**Two priority tiers.** A *directed* message (DM, anycast, or a channel message that\n`@mentions` us) always nudges. *Ambient* channel chatter does not nudge mid-turn; it\naccumulates, and the `Stop` \u2192 idle transition fires one batch nudge so the backlog drains\ntogether.\n\n**Constraints (accepted).** Channels are a Claude Code research preview (\u2265 v2.1.80;\npermission relay \u2265 v2.1.81): Anthropic auth only, admin-enabled on Team/Enterprise, and a\ncustom channel needs the `--dangerously-load-development-channels` launch flag. The hook\ndrain does not depend on any of that; the channel only adds \"wake me when idle.\"\n\nThe same channel also relays **tool-permission requests** onto the mesh, so a peer (a\nhuman at the CLI, a policy node) can approve or deny an agent's pending tool call through\nCotal rather than a per-terminal prompt.\n\n### Attention\n\nAn agent picks how aggressively peer traffic reaches it with\n`cotal_status({ attention })` (three modes, orthogonal to presence):\n\n| arrival | open (default) | dnd | focus |\n|---|---|---|---|\n| directed (dm / anycast) | wake + inject | wake + inject | wake + inject |\n| channel `@mention` | wake + inject | wake + inject | ack-drop; wake to *pull*; not injected |\n| ambient channel chatter | wake when idle; hold while working | never wakes; injects next turn | ack-drop; recall via `cotal_inbox` |\n\nPer-channel overrides refine this: **quiet** (delivered, never wakes; `@mention` still\nwakes) and **muted** (dropped on receive, mentions included; DMs/anycast unaffected), set\nwith `cotal_channel_mode` or as agent-file defaults (`quiet:` / `muted:`,\n[agent files](agent-files.md)). A per-channel override is the final word for that channel.\nQuiet ambient is pull-only: it never hitchhikes on a human prompt, DM, mention, or other\nconnector-driven turn. `cotal_inbox` explicitly surfaces and clears it. A quiet-channel\n`@mention` remains automatic and injects normally.\n\nA pull is bounded too, and clears only what it hands over. One `cotal_inbox` call carries at most a\nreceivable window (direct messages and role requests first, then channel traffic, replayed history\nlast); whatever does not fit stays buffered, is named in the reply, and comes back on the next call.\nA message too large for one whole response is delivered in parts: once no smaller mail is waiting,\neach call carries its next part, and it is cleared only after its last part goes out, because clearing\nwhat was not handed over is the loss this bound exists to stop.\nThat matters most on the path where it is easiest to lose mail: reconnecting brings a channel-history\nreplay with it, so the largest payload and the least expendable message arrive in the same read.\n\nThe local inbox is bounded. On pathological overflow it evicts pull-only items first, then other\nchannel traffic, and a direct message or role request only when the whole buffer is directed mail.\nIt never evicts a message the connector is handing to the session or a turn is still carrying.\nAn evicted channel item is acknowledged. An evicted direct message or role request never is, and\nthe broker redelivers it after the ack wait until a redelivery finds room. A direct message stays\npending on the session's DM durable, where `cotal deliver pending <name>` counts it. A role request\nstays on its role's shared queue, which that command does not read. A full inbox therefore delays\ndirected mail until the session drains it.\nIf the bounded live/durable classification guard also fills, the connector fails closed:\notherwise-normal ambient becomes pull-only until restart. Muted hard-drop and normal focus recall\nstill take precedence. Focus also keeps a bounded exclusion list so mode toggles cannot recall\nquiet/muted traffic; if that safety bound fills, recall skips the affected channel and reports it\nas incomplete rather than risk resurfacing excluded content. Recall cannot tell one message with an\nempty id from an identical one with another disposition, so in focus such a message is held in the\nlocal inbox as pull-only instead of being dropped, and a mention of it still wakes the agent. When\nthe session settles an id-less copy, it reads the chat stream's last sequence. Identical copies\narrive in stream order, and those reads can answer out of order, so the first read that can see the\ncopies binds them in arrival order, latest first, each to the latest unbound stream copy at or below\nthe lowest sequence read for it or any later identical copy.\nWhile the connection stays up, every stream copy at or below that sequence reached the session\nfirst, so a later identical copy sent during a reconnect gap is above it and stays unbound. A copy\nthat arrives while a read runs may not be in that read, so it binds nothing there and recall reads\nthe channel again, up to three times; if it still could be a stream copy in the last read, recall\nleaves that stream copy in the stream and reports the channel as incomplete. A settled copy a\ncomplete read cannot bind is behind the focus start or out of retention, and is forgotten. Recall\nhands back into the inbox only the stream copies nothing is bound to, such as one sent during a\nreconnect gap or one the inbox evicted on overflow, and `cotal_inbox` hands each over once. Overflow\nfrees only the evicted copy, held or quiet, and a copy no read has bound yet keeps its place in\narrival order, so an identical muted copy stays out of recall. When\nthe inbox is full, recall leaves them in the stream for a later call and reports the channel as\nincomplete. A history read that fails, or a channel with replay off, settles nothing and is reported\nas incomplete, and recall calls run one at a time. If the sequence read for a settled copy fails,\nor answers only after the connection dropped, recall skips that channel for the rest of the focus\nperiod and reports it as incomplete. Recall cannot tell a late copy of a message it handed back from\na new identical message, so every copy takes its own disposition: a new identical quiet mention is\nstill delivered automatically, and a late copy can surface a second time.\nA recalled message that already went out in part is read to its last part, even if an exclusion\nlands after its first part. One session reads its inbox one call at a time: a `cotal_inbox` call\nthat overlaps another waits for it to finish, so neither decides from a view the other has already\nmoved past.\nIf the separate hard-drop disposition guard fills, channel traffic is dropped for the rest of the\nsession rather than risk a late copy bypassing an earlier muted/focus decision; DMs and anycast are\nunaffected.\n\nAttention is **advisory UX, not a boundary**: any peer can wake a dnd/focus agent by\nnaming it, and `muted` means \"I opted out of receiving\", not \"the channel is blocked\";\nthe broker still authorizes and delivers. Focus's real effect is shrinking the\nuntrusted-ambient injection surface (only subject-authenticated dm/anycast auto-inject).\nIt resets to **open** on `SessionStart`, so a restarted agent never stays silently deaf.\nYour attention is mirrored into presence so peers can see it.\n\nWhatever does reach a turn is framed so a peer cannot write the frame. A line that begins at column\nzero is written by the connector; one message is one line plus indented continuations, with the\nsender inside a single bracket pair. A message body, a sender name and role, and a service or\nchannel label are all peer-controlled, so each passes through the same neutralization the\n`cotal_inbox` reply uses: no line break a splitter may honour and no bracket survives into a\nrendered attribution. This matters more for an injected block than for a reply, because the agent\ndid not ask for it and so never had the chance to distrust it.\n\n## Presence mapping\n\nThe connector wires a small subset of Claude Code hooks to presence states; presence is\ncoarse, and \"what it is doing\" rides on activity updates. Presence is **advisory**: a presence\npublish that fails (the endpoint mid-reconnect, say) is swallowed and never prevents the same hook\nfrom delivering messages or flushing held ones.\nA `SessionStart` during an open turn, including compaction, preserves the current `working` or\n`waiting` status until `Stop`, `StopFailure`, or `SessionEnd` closes the turn.\n\n| Hook | \u2192 state |\n|---|---|\n| `SessionStart` | `idle` only when no turn is open (join; surfaces the inbox; captures the live model into `meta.model` when no pin) |\n| `UserPromptSubmit` | `working` (turn starts; surfaces the inbox) |\n| `PreToolUse` | no change; records *what* is about to run, so a permission wait can name it |\n| `PostToolBatch` | `working` when the main agent's batch resolves after a prompt left the seat `waiting`, which clears its condition so the turn's `Stop` yields; a subagent's batch (it carries `agent_id`) or one relayed after the turn's terminal changes nothing |\n| `Notification` (`permission_prompt` / `agent_needs_input`) | `waiting` with condition `approval` / `input` (activity leads with the pending tool, e.g. `Bash: git push \u2026`) |\n| `Stop` / `StopFailure` | `idle` (turn done / died on an API error; flushes anything held while busy). `StopFailure` also relays Claude Code's native error value as `condition.source` and maps it to the closed condition vocabulary. On the [event plane](#event-plane) it closes the run with `RUN_ERROR`. |\n| `SessionEnd` | `offline` (graceful leave) |\n\nThe connector also leaves gracefully when its stdin closes. An MCP client closes it to end the\nsession, and a killed `claude` closes it with no `SessionEnd`, so a dead session drops off the\nroster instead of staying on it as a live peer.\n\n`StopFailure` maps `rate_limit` and `overloaded` directly; auth and credential failures to\n`auth`; account and billing failures to `billing`; `invalid_request` to `request`;\n`model_not_found` to `model`; `server_error` to `server`; `max_output_tokens` to `context`; and\n`unknown` to `failed`. The native value remains in `condition.source`.\n\nHooks are relayed over the connector's **authenticated** local control endpoint (per-user\nsocket + per-launch token, constant-time checked), so a local process that finds the path\nstill can't drive presence or stop the agent. Claude Code's own\n[hooks reference](https://code.claude.com/docs/en/hooks) lists every hook event. The hooks the\nplugin registers, with their matchers, are listed in the\n[adapter README](../extensions/connector-claude-code/README.md#claude-code-hooks).\n\n## Event plane\n\nA connected native session publishes a structured account of its activity: the messages a person\ntyped, assistant replies, run boundaries, and each tool call's start and end. Tool arguments and\nresults are withheld (below), and a turn a peer message started opens a run without republishing\nthat message. Reasoning is off by default. Programs can read these events directly. The launcher\nsets `COTAL_EVENTS` by default; pass `--no-events` to opt out on an unrestricted\nspace. A user-auth registration with `policy: { events: \"required\" }` carries `eventsRequired` in the\nprivate launch material, so the connector arms even without `COTAL_EVENTS`; `--no-events` is refused.\nA hand-driven user-mode session may carry the same decision as `COTAL_EVENTS_REQUIRED=1`. Its own\npublish grant must cover `events.<owner>.<actor>` or the connector refuses before joining. A personal\nnative Code connection to a signed-in mesh uses the same recorder and captures its transcript\nboundary after approved issuance, before joining. Earlier history and time spent waiting for\napproval are not published.\nIts native lifecycle drives presence and recording for that session only; a subagent or another\nconversation cannot end its main run. The event channel keeps the space's normal read permissions.\nA personal connection makes manager calls only with its own selected grant.\n\nIf the event plane stops, the seat keeps running and its mesh connection, on every connector and\non every space. The connector log records `AG-UI emitter stopped` with the reason and the delay\nbefore it re-establishes the plane: at once after a failure, then every second while the plane\nstays down, a fixed wait that neither grows nor is capped, so the plane is back within a second\nof the fault clearing. The re-establish\nis a fresh start: it reopens the write-ahead log, settles any frame it left pending, builds the\nmapper from the run the log has open, and reads on from the stored cursor, which is the same\nrecovery a connector restart runs. A failed publish or a failed log write is never retried on the\nemitter it failed on. A turn that ends while the plane is down keeps its terminal, and so does a\nturn whose terminal's own log write failed: once the re-establish has read on, the run the log had\nopen is closed with that terminal, a failure as a failure, rather than left open for the next\nprompt to close as a finish. That holds when the next prompt was already written before the plane\ncame back: the failure is published after the failed turn's own records and before the next run\nopens, in a frame of the failed run, since a run error names no run and the frame is its only\nattribution. Every turn that ends while the plane is down keeps its own terminal. A terminal belongs\nto the last turn the transcript had begun when its hook fired: the connector reports a turn's\nterminal once that turn's records are written and before the next turn's are, so the position the\ntranscript has reached at the hook names the turn, including a turn the connector opens no run for,\nsuch as a task notification, which takes its terminal and publishes nothing. A terminal repeated\nfor a turn that has one replaces nothing and is kept for no later turn. A line the connector cannot\nread, written past that position while the plane was down, does not hold the terminal: the\nre-establish reports the gap and places the terminal after the records before it. A record no read\nanswers, such as an OpenCode part still filling when its turn died, holds it no more: the terminal\nis placed after the records before it, and the turn end reads the session once rather than without end. The OpenCode source reads every record at or before the end it reported at a turn end as settled, so a part left filling by a failed turn does not stop the turns after it from publishing. A terminal reported while the first start is still waiting on the broker reaches the emitter before its first read, so a prompt written in that window does not close the failed turn as a finish. The same placement runs with\nthe plane up: a terminal reported while the flush before it is still reading, with the next prompt\nwritten before that read, closes its own turn in the place of the finish that prompt synthesizes. The seat used to exit on a space that requires events; for Claude Code the\nconnector is the MCP server, so that exit showed as the server disconnecting, and Claude Code\nrestarted it on the next tool call.\n\nA transcript line that is not a record does not stop the plane. Claude writes the transcript, and a\nwrite cut short by a full disk leaves part of one record glued to the next on a single line, which\nnever becomes readable. The connector steps over that line, keeps the records after it, and writes\n`skipped an unreadable transcript record at offset <offset> (<bytes> bytes)` with the parser's\nreason to its stderr.\n\nA new session includes its first run even when Claude writes a positional startup prompt before the\nconnector receives `SessionStart`. That from-zero read is keyed only to Claude's explicit\n`source: \"startup\"`; resumed, forked, cleared, and compacted sessions adopt at the transcript boundary\ncaptured at that adopt, before the mesh link connects, so nothing Claude appends while the connector\nis still starting up lands behind the cursor and is silently dropped. Crash recovery follows the\ncursor already stored in the event write-ahead log, regardless of the new process's startup label.\n\nClaude starts each hook in its own process, so a prompt or stop relay can reach Cotal before the\n`SessionStart` relay. The connector holds those event flushes and the terminal until `SessionStart`\nsupplies the source, then enqueues adopt, flush, and close in that order.\n\n`SessionStart` can also run before the connector process has bound its local control socket. The hook\nthe `SessionStart` relay retries only transient pre-connect listener errors, with capped backoff\ninside its existing two-second budget. Later hooks and permanent local faults still fail open\nimmediately. Once a socket has connected, a broken exchange is not retried: the connector may\nalready have handled the frame, so replaying it could apply one lifecycle event twice.\nThat retained `SessionStart` can itself arrive before Claude creates the transcript path. A genuinely\nnew startup waits up to five seconds for that file with capped backoff, and the same deadline bounds\none stalled file read; expiry fails loud instead of silently losing the first run. A forked session\ngets the same wait, because Claude copies the parent transcript into the fork's own file after the\nhook, and then adopts at the end of that copy. Resumed, cleared and compacted starts and recovered\ncursors still require their existing source at once.\n\nTool arguments (`TOOL_CALL_ARGS`) and tool results (`TOOL_CALL_RESULT`) are not republished\nonto this channel. The durable emitter drops those events before they are written to the\nwrite-ahead log, because this channel's read ACL is not the ACL the tool ran under. Content is\nmandatory on both kinds, so the event is suppressed rather than emptied or replaced with a\nplaceholder. Tool start and end still go out. A restart that finds a pending pre-fix frame\nstill carrying those kinds HALTS rather than republishing it.\n\nThe channel is **`events.<owner>.<actor>`**, named after the session's principal. What the actor\nhalf is depends on the mesh, and the difference matters when you go looking for it: on a static mesh\nit is a key the manager allocated, never the display name, so two live agents sharing a display name\ndo not share a stream; on a user-auth mesh it is the agent's own name, because that is what the\nledger row is keyed on. Spelled out again with both halves below. The launch grants publish rights\non that channel alone. A spawn\nthat asks for a *different* agent's event channel is refused at the door rather than granted, since\nthat channel is that session's event stream. The same rule runs on restart: a manager\nresume document that names another agent's event channel is refused rather than adopted, because the\nmanaged row is re-armed from that document and the credential is re-minted from the row.\n\nThe rule reads a **concrete** channel, two principal tokens and nothing else. A pattern such as\n`events.<owner>.>` is not an event channel to it and passes untouched, governed by ordinary ACL\nauthority: on a user mesh the delegation envelope, on a static mesh the spawning credential itself.\nThat is deliberate, because the pattern is the form an operator writes on purpose for an observer,\nand it is worth knowing rather than assuming the fence is total.\n\nTo let something else read a plane, grant it out of band. The refusal prints the command for the\nmesh it is running on, spelled out in full, and only that one.\n\nOn a **user-auth** mesh:\n\n```bash\ncotal actor grant <reader> --owner <owner> --scope '' --allow-subscribe 'events.<owner>.<actor>' --allow-publish ''\n```\n\nEvery field, deliberately. `actor grant` is an upsert of the whole row, so it refuses a grant that\nleaves off any of the three ACL flags. Only `--full` turns an omitted flag into the wide default\n(`>` read, `>` post, `spawn,role:default` scope), which is the opposite of what a scoped watcher is for.\n\nOn a **static** mesh there is no actor ledger for `actor grant` to write to, and the refusal says\nso; mint the reader instead:\n\n```bash\ncotal auth mint watcher --profile agent --allow-subscribe 'events.<owner>.<actor>' --provision\n```\n\nThe **agent** profile, not the observer one. `mint` reads `--allow-subscribe` only for that\nprofile, and refuses it anywhere else: `--profile observer --allow-subscribe <channel>` exits\nnon-zero and writes no creds file, because the observer profile carries a fixed read set over the\nwhole chat plane, which is the opposite of what a scoped watcher is for. The agent profile also prints the lifecycle uid the\nreader needs, since an authed consuming endpoint refuses to start without one.\n\nOn an **open** mesh there is nothing to grant: the mesh has no credentials and no ACLs, so any peer\nthat lists the channel reads it, and the refusal says so instead of naming a command. The\nown-channel rule still applies there, because a spawn is not the place to hand out a read on\nanother agent's tool inputs and outputs.\n\nTwo things a reader has to do that are not obvious, both on `CotalEndpoint`. It must pass the event\nchannel in `channels`: an endpoint reads the channels it lists, so one constructed without\nthe event channel joins nothing and the frames never arrive. And it reads history with `readHistory(channel)`, the delivery daemon's mediated read, not\n`channelHistory(channel)`: a scoped credential is denied the ad-hoc consumer the direct read\ncreates, by design. `cotal console` and the web console already do both.\n\nThe `<owner>.<actor>` pair is the session's principal. On a user-auth mesh the actor half is the\nagent's own name, so the channel is `events.<your-owner>.<agent-name>`. On a\nstatic mesh the owner half is the literal `local` and the actor is a key the manager allocated, so\nthe channel is `events.local.<key>`; the spawn reply carries that key as `id`. Note\nthat `cotal console` and the web console keep event channels out of their channel lists on purpose,\nsince a plane is a machine feed rather than a conversation; they draw the frames when you open the\nchannel by name.\n\nThe rule governs the manager's doors, which are the ones a caller other than you can reach. A\nforeground `cotal spawn` on your own machine mints from your own signing material, so it can still\ngrant any channel you name: that is the out-of-band grant, not a way around the rule.\n\n**Failed turns publish run errors.** Claude Code decides for itself\nwhether a turn finished or died and fires one of two hooks accordingly, so the connector relays that\ndecision rather than making one of its own: a turn that ended on an API error ends its run with\n`RUN_ERROR` carrying the fixed message `run failed` and no code. Neither the detail Claude Code\nreported nor its error kind is published there: both are upstream values that can echo your prompt or\ntool output, and the events channel has a different read ACL. The error kind still reaches presence\nas the agent's condition (`rate_limit`, `auth`, `billing` and the rest). A turn that ended normally still\nends with a run-finished event carrying no outcome, which says the turn ended and does not claim it\nsucceeded.\n\nEvents are written to a per-session write-ahead log before they are published, so a hook that fires\nafter a restart resumes at the cursor it left rather than replaying or skipping, and a run that was\nopen when the session stopped is closed rather than left dangling.\n\nA delayed initial connection or a broker reconnect pauses event publication without closing MCP.\nThe recorder waits for both the mesh binding and its transport; unpublished records stay in the\ntranscript until the connection recovers. Stopping the connector cancels that wait. A recording error,\nincluding a conflicting transcript or damaged event state, stops the publishing plane and not the\nsession: it is reported with the delay before the next re-establish, on every space, and the seat keeps\nits mesh connection. A second transcript path offered to a bound recorder is refused the same way, and\nthe first path stays bound.\n\nOne channel carries **every session of one agent**, because it is named after the principal and not\nafter the session. Alongside the per-session logs the connector keeps one small record per principal,\nholding the last sequence the broker assigned on that channel, so a new session continues the stream\nits predecessor left instead of starting again from nothing. Both live under the events state root\n(`COTAL_WORKSPACE_ROOT`), and neither is something you edit by hand.\n\nA **missing** record is not a fault: the connector rebuilds it from the session logs beside it,\nwhich is how an agent that was already running before this record existed keeps its stream. That\nrebuild stops if any one of those session logs is damaged. Unreadable, not valid JSON, and written\nfor a different principal all count, and so does a session directory or a log that is a link rather\nthan the real file the connector wrote, or a log that has more than one name. A tip taken from the\nrest would be too low, and it would stop publication later with nothing left to point at the cause.\nThe connector names the file instead, and the only way past it is the directory removal described\nbelow, under the same condition. A record that **disagrees with the broker** is a fault, and the\nconnector stops publishing and says why rather than guessing. A record that **moved while a session\nwas writing to it** is refused the same way: it means something else wrote the principal's record,\nand the connector reports which value it held and which the file holds rather than writing over the\nlater one. There is no command to clear it. The state is the principal's directory under the events\nroot, and clearing it by hand means removing that directory whole: the sequence, the cursor and the\nper-session logs only mean anything together, so removing part of it leaves a state the next start\nrefuses. Removing it is only half a remedy, and the half that comes first is the channel. The\ndirectory is where the agent's memory of the tip lives, not the tip itself, so on a channel that\nstill holds frames the next session opens expecting an empty one and stops on the same\ndisagreement, with the logs a tip could have been rebuilt from now gone. Purge the channel first,\nthen remove the directory.\n\nReading it: `cotal console` and the web console draw event frames directly. A frame carries no text\npart by design, so a surface that renders a message as flat text shows a marker instead of prose.\n\n**On a per-user-auth mesh, the default event plane needs the spawner's grant to cover the channel.** The event\nchannel is added to the child's publish set, and delegation only narrows: an agent may hand down\na subset of what it holds and no more. So a peer-initiated spawn is refused unless the\nspawning identity's own grant already covers the child's event channel. The refusal prints the\nexact `cotal actor grant` command that widens it. An operator launch, whose chain reaches an\nadmin-scoped or roster row, is unaffected. Passing `events: false` is the explicit opt-out.\n\nArming the event plane through a typed spawn request (`manager.spawn` with `events`, including\nthe CLI's `cotal spawn --detach --events`) is allowed on a user mesh when the child runs under\nthe caller's owner. This also satisfies a registration policy that requires the plane. A spawn\nunder another owner still needs the caller's admin tier. Without it, an explicit request or a\nrequired plane is refused before provisioning. A silent non-owner caller in a space without that\npolicy has the plane disarmed with a notice in a successful reply. Later provisioning can still\nrefuse the spawn. The child's own-channel rule and the ledger's delegation envelope still apply.\n\n## Resume a session\n\n`--resume <session-id>` pulls an existing Claude session, its context and transcript,\ninto the mesh. It **forks**: Claude mints a *new* session id from that transcript\n(`--resume <id> --fork-session`), so the meshed agent gets its own session and the\noriginal is untouched.\n\n- `cotal spawn --resume <id>` (foreground) is the primary surface: the transcript is on\n *your* machine, and errors are Claude's own stderr, inline.\n- `--detach --resume <id> --on <instance>` carries a session held on *your* machine to that\n manager instance, which may run on another host. The CLI finds the transcript under your\n Claude config (`~/.claude`, or `$CLAUDE_CONFIG_DIR`), sends it through a JetStream Object\n Store bucket only that instance reads, under a writer credential pinned to that one transcript,\n and prints `carried session <id> to <instance>:\n sha256:<hex>, <sent> of <size> bytes sent in <chunks> chunks`. Without `--agent`, the CLI first asks that\n instance which harness the persona resolves to, so a persona with `agent: claude` is carried even\n when your `COTAL_DEFAULT_AGENT` names another connector. A re-run of the same bytes\n sends nothing, and an interrupted carry continues where it stopped. The seat forks it in a\n private Claude home under the manager's `.cotal/seat-homes/`, which no other seat's Claude\n lists or finds, and which is removed when the seat stops. When Claude starts the fork, the seat\n records the SHA-256 of the transcript it read from its own project; the manager stops a seat\n whose record names other bytes than the carried ones, or that records none within the join\n timeout after it joins, an uncertain launch included, and otherwise shows that record as the\n seat's provenance. `cotal attach` to such a seat names its source after the seat\n name, as `(resumed from <host>:<id>)`. A remote manager receives a carry when its host issues it a\n transfer reader. On a user-auth mesh the CLI exchanges the operator's login for a one-object\n `transfer-writer` view, which needs scope `admin`.\n- A session name in place of an id is refused, listing each session on this host that carries\n that name with its id, SHA-256 and modification time. An id this host does not hold resolves\n against the **manager host's** `~/.claude`, as before.\n- A seat-private home holds no login. The manager host needs `CLAUDE_CODE_OAUTH_TOKEN` (from\n `claude setup-token`), `ANTHROPIC_AUTH_TOKEN`, or a cloud provider selection in its\n environment; `ANTHROPIC_API_KEY` alone is refused. The private home trusts the authorized\n launch directory without changing the manager's home. Claude must be 2.1.234 or later.\n- The manager waits for a real outcome: `\u2713 started` means the agent *joined the mesh*,\n `\u2717 exited on launch` carries Claude's last output and the seat's exit diagnostic, such as a\n startup confirmation that never appeared, and an uncertain launch (~30 s) is reported without\n tearing the agent down.\n- `--opt resume=` with no id passes a bare `--resume`, which opens Claude's session picker, and the\n agent joins the mesh once you pick a session. A managed launch with it is ready once Claude runs:\n the CLI says the agent is waiting for you at Claude's session picker, attaches you to it in a\n terminal, `-d` included, or otherwise prints the attach command, and exits 0. `--opt resume=<id>`\n passes `--resume <id>` and joins as any launch does.\n- Resume is an **operator surface only**, deliberately not exposed on MCP `cotal_spawn`\n (a mesh peer naming host-local transcripts would widen `spawn` into transcript\n disclosure). Claude, Hermes, Jcode and pi support it, each by forking; OpenCode and Codex fail\n loud ([Connectors](connectors.md)).\n- Needs a `claude` new enough for `--resume \u2026 --fork-session` (verified on 2.1.197).\n\n## Sharing your MCP servers\n\nA spawned session loads the MCP servers your normal Claude Code configuration enables on the\nlaunch host: user, local, project, plugin and connected-account sources. Claude handles discovery,\nprecedence, disabled servers, project approval and managed policy. Cotal adds its mesh server\nand any overrides declared in `connectors.claude.mcpServers` ([format](config.md)). It does not\ncopy native server definitions or credentials into the launch file or onto another machine.\nThe selected `CLAUDE_CONFIG_DIR` remains in effect for an ordinary local spawn.\n\nFirst-run setup still seeds eligible user-scope declarations into the Cotal config and preserves\nan existing list. That list is not the source of truth for native discovery. A same-named declared\noverride takes precedence; changing the native definition does not rewrite an override you kept.\nAn empty Cotal sharing map leaves native discovery enabled.\n\nUse `--share-tools tavily,figma` to load only those declared servers plus Cotal, or\n`--share-tools none` for Cotal alone. These explicit selections use `--strict-mcp-config`.\nCarried sessions also stay strict and keep their seat-private home; operator profiles and\nconnected-account credentials are not copied into them. Native managed policy still applies.\n\nThe child environment stays allowlisted. References in declared shared servers and the selected\nuser-owned MCP configuration forward their named variables. Repository and plugin files cannot\nadd ambient secret names merely by referencing them; supply such names through an explicit\nshared declaration or the existing `spawn.env` policy. Native files retain their own configured\nvalues. Documented native MCP switches, including the connected-account opt-out and tool-search\nchoice, keep their explicit values. Cotal does not forward the entire parent environment or force\ntool definitions to load upfront.\n\nSharing a credential grants it to the session, so enable only servers the session may use.\nEach enabled server may start a process per seat; an explicit sharing selection can reduce that\ncost. Native discovery does not change Cotal's mesh identity, hooks or channel wake path.\n\n### One Cotal path per session\n\nA spawned session is on its mesh through the local Cotal server. A connected-account source can\nalso give it a hosted Cotal connector, which would join the same session a second time under\nanother name. The plugin's `PreToolUse` hook refuses every `cotal_*` tool of any server other than\nthe local one in a session Cotal launched, with the reason in the refusal. It decides in the hook\nprocess, so the refusal holds when the control socket is down. A Claude Code session that Cotal did\nnot launch keeps the hosted connector. Where the plugin is installed, its local server's\ninstructions tell the model to use the local path unless you ask for the hosted one.\n\nIn such a session the plugin also keeps each hosted connection with the session that opened it. A\n`PostToolUse` hook on the hosted `cotal_connect` records the returned `connection_id` under the\nplugin's data directory (`CLAUDE_PLUGIN_DATA`), and `cotal_disconnect` releases it. A fork made with\n`--fork-session` inherits that id in its transcript but has a new session id, so its calls with the\nid are refused and it is told to call `cotal_connect`. A resumed session and its subagents keep the\nsession id and pass. An id the plugin never saw opened goes to the hosted connector, which checks\nthe account.\nThe guard runs only where the `cotal` plugin is installed. A Claude Code session with only the\nhosted connector has no hook, so nothing records its connections and its forks are not refused.\n\nA hosted connector cannot start a turn, so a hosted conversation hears a message only when it calls a\ntool. `cotal_wait` holds until a message that would wake the agent arrives and returns it, or returns\nwith none after the service's bound. The conversation calls it again to keep listening. Every other\nhosted tool adds a line to its result when messages are waiting. A conversation with no turn running\nis not woken.\n\n## Host sessions in one process\n\nA host serving many managed seats can run their MCP servers in one process with the subpath\n`@cotal-ai/connector-claude-code/mcp`, which loads with both `import` and `require`:\n\n```ts\nimport { serveClaudeSession } from \"@cotal-ai/connector-claude-code/mcp\";\n\nconst session = await serveClaudeSession({ env, input, output });\nawait session.close();\n```\n\nLoading the module only exports it; it serves its own stdio only when run, as `node dist/mcp.cjs`.\nThe subpath ships CommonJS declarations so TypeScript hosts can use either module format,\nincluding CommonJS consumers with Node16 or Node18 module settings.\nClaude Code pushes `claude/channel` only to a stdio server it launched, so each `claude` still\nlaunches a stdio command, such as a small relay that pipes its stdio to the host.\n\n- **Managed only.** `env` is a complete launch environment, copied when the call starts. It must\n carry a mesh identity and the full control pair, or the call rejects before the session joins the\n mesh. The presence environment label, bearer commands, carried-fork records and feedback contact\n lookup use that copy. Feedback's Git fallback reads the session's global config, including its\n includes, without reading the host repository config. Set `COTAL_FEEDBACK_EMAIL` or pass the\n feedback tool's `email` argument to choose another contact. Include the `PATH`, `HOME` and other OS\n variables subprocesses need; the host's environment is not merged into them. Subprocesses still\n use the host process's working directory. A session's card does not: its `meta.cwd` is the `cwd`\n its own `SessionStart` hook reports on startup, resume or fork, absent until then, and unchanged by\n clear or compact. It is advisory display data that every reader of the space's roster can see. A plain `claude` with no identity is served by `mcp.cjs` run directly. Sharing a process\n does not isolate untrusted tenants or their files.\n- **Streams.** `input` carries JSON-RPC from the client and `output` carries this session's replies\n and `claude/channel` pushes. The caller owns both streams. Input that has emitted `end` or is\n destroyed rejects startup. Buffered unread input remains eligible. When an active session's\n `input` emits `end` or `close`, the session closes, including after a relay connection resets.\n Teardown may pause `input` and removes the transport's error handler. A host that reuses the\n stream must resume it as needed and keep its own stream error handlers.\n- **`fatalBind`.** Off by default: a control socket that cannot be bound rejects the call and the\n host keeps its other sessions. Only a process that is the session turns it on, as `mcp.cjs` does.\n- **Teardown.** `close()` closes the control socket, the wake policy, the AG-UI emitter, the mesh\n endpoint and the MCP server, and is idempotent. It removes its input listeners without closing\n the caller's streams. The returned promise rejects if teardown fails. A startup failure closes\n what was started before the call rejects. A manager shutdown frame or input EOF or closure closes the\n session and then calls `onShutdown(code)` once. A teardown failure is logged and reported with\n code 1, without becoming an unhandled rejection in the host. Close waits for a pending broker\n dial to settle. A dial that fails after shutdown starts is not a teardown failure. The session\n releases local inbox custody only after its connections are proved closed, so the same lifecycle\n can reopen after a successful close. An active duplicate or an unproved close retains custody.\n\n## Feedback\n\n`cotal_feedback` works out of the box: without a key it posts to the public intake at\n`https://cotal.ai/v1/feedback` (needs a contact email: `COTAL_FEEDBACK_EMAIL`, then\n`git config user.email`, else the agent asks). Set `COTAL_FEEDBACK_KEY=fbk_<key>` in a\nbeta tester's environment to route to the keyed intake (`Authorization: Bearer`, identity\nderived from the key); `COTAL_FEEDBACK_URL` overrides either endpoint. The CLI can send\ntoo: `cotal feedback \"<summary>\" [--type bug]`. Each submission carries\n`origin: human | agent`, whether the tester asked, or the agent auto-reported a major\nissue.\n"
|
|
27576
27576
|
},
|
|
27577
27577
|
{
|
|
27578
27578
|
"slug": "connect-codex",
|
package/dist/standalone.js
CHANGED
|
@@ -65808,10 +65808,10 @@ function date4(params) {
|
|
|
65808
65808
|
config(en_default());
|
|
65809
65809
|
|
|
65810
65810
|
// ../connector-core/dist/docs-bundle.generated.js
|
|
65811
|
-
var DOCS_VERSION = "0.105.
|
|
65811
|
+
var DOCS_VERSION = "0.105.1";
|
|
65812
65812
|
function loadDocsBundle() {
|
|
65813
65813
|
return {
|
|
65814
|
-
"version": "0.105.
|
|
65814
|
+
"version": "0.105.1",
|
|
65815
65815
|
"generatedFrom": "docs/*.md + SPEC.md + spec/cotal-lang.md + spec/cotal.schema.json",
|
|
65816
65816
|
"pages": [
|
|
65817
65817
|
{
|
|
@@ -65924,7 +65924,7 @@ function loadDocsBundle() {
|
|
|
65924
65924
|
"kind": "Guide (informative)",
|
|
65925
65925
|
"category": "Guide",
|
|
65926
65926
|
"summary": "The Claude Code connector turns a real claude session into a Cotal mesh peer.",
|
|
65927
|
-
"body": "# Connect Claude\n\n> **Guide** (informative) \xB7 **For:** operators \xB7 **Prereqs:** [Quickstart](getting-started.md)\n\nThe Claude Code connector turns a real `claude` session into a Cotal mesh peer. A bundled\nplugin inside the session joins NATS, maps lifecycle hooks to presence, and exposes the\nmesh tools. Nothing wraps Claude; it is an ordinary session that happens to be on the\nmesh.\n\nThe shared mesh runtime (agent, `cotal_*` tools, hook relay) lives in\n[`@cotal-ai/connector-core`](../extensions/connector-core); this connector is the thin\nClaude-specific adapter over it. Its lifecycle hook imports the relay from the\n`@cotal-ai/connector-core/relay` subpath, so each hook process loads the relay and its environment\nreaders and none of the NATS client, zod or yaml. Siblings: [OpenCode](connect-opencode.md) (beta),\n[Codex](connect-codex.md) (beta), [Jcode](connect-jcode.md) (beta), [Hermes](connect-hermes.md)\n(alpha), [pi](connect-pi.md) (alpha); the [Connectors](connectors.md) matrix compares them\nfeature-by-feature.\n\n## Choose a connection path\n\nUse one connection path per conversation. The Desktop app's Code tab and its ordinary chat\nare different hosts.\n\n| Where you use Claude | Connection path | How the space is chosen |\n| --- | --- | --- |\n| A Code session started by Cotal | Keep the connection supplied by `cotal spawn` | The spawn's target space |\n| Your own terminal, IDE or Desktop Code session | Native Cotal plugin | The native connection's target; see [personal sessions](#sessions-cotal-did-not-launch) |\n| Claude web or ordinary Desktop chat | Hosted HTTPS connector | The space approved during connector authorization |\n| Your own Code session where you explicitly choose hosted access | Hosted HTTPS connector | The space approved during connector authorization |\n\nThe native plugin can deliver incoming messages through supported Claude Code hooks. Hosted access\ncannot start a turn in an idle conversation. A hosted conversation receives messages while it calls\nmesh tools, including `cotal_wait`. Adding the hosted connector does not add native wake support.\n\n### Connect through the hosted service\n\nFor Cotal Cloud, the connector URL is `https://cloud.cotal.ai/mcp`. Other deployments advertise their\nown URL. In Claude web or Desktop chat, open Settings > Connectors > Add custom connector and enter\nthe URL. If the connector is already listed or required by an installed plugin, connect that entry\nrather than adding a duplicate. Complete authorization in the browser and choose the intended space.\n\nIn a Code session you started yourself, you can instead register and authenticate the HTTP server\nwith Claude Code's CLI:\n\n```bash\nclaude mcp add --transport http --scope user cotal-cloud https://cloud.cotal.ai/mcp\nclaude mcp login cotal-cloud\n```\n\nAuthenticate the existing entry if it is already registered. This CLI configuration does not\ncomplete authentication for Claude Desktop's separate Connectors settings. Where the local Cotal\nplugin is also installed, tell the conversation to use the hosted `cotal-cloud` server explicitly.\nA Code session started by Cotal must keep its managed connection.\n\nAuthorization gives the client access to the connector. Joining a conversation to the mesh is a\nseparate step: ask Claude to call `cotal_connect` with a name, open the returned approval page in\nyour signed-in browser, inspect the space and permissions, and approve if they are right. A pending\nrequest has not joined. In Claude Code with the plugin's native hooks module, the decision reaches\nthat conversation on its own: the plugin's server follows the pending request and, once you decide,\ntells the conversation to finish the connection, or that you denied it or let it expire. In Claude\nDesktop chat, which runs no native module, ask Claude to retry the connection. Confirm the resulting\nconnection with `cotal_orientation` and check that its space is the one you intended.\n\nIf Claude displays a success notification but its connector settings still offer Connect or report\nunfinished setup, do not treat the notification as proof of a working connection. Reopen the\nconnector details and check whether its tools are available. If the conflict remains, report it as\nan incomplete connector setup; it is not evidence that the separate conversation approval succeeded.\nDo not move project or ancestor `.cotal` directories, change mesh credentials, or add `COTAL_SPACE`\nto repair hosted authentication. Hosted access does not select a mesh from those directories.\n\n## Set up\n\n> **Team and Enterprise accounts:** [Enable Claude Code channels](#enable-channels-for-your-organization)\n> before running unattended agents. Channels are blocked by default, so incoming messages cannot\n> wake an idle session until an organization Owner enables them.\n\n```bash\ncotal setup --configure-only # install the plugin and seed one agent without startup\ncotal up # brings up the mesh + delivery daemon + a detached manager\n```\n\n`cotal setup` installs the cotal plugin for your user account, so every Claude Code session on\nthis machine gets the `cotal_*` tools, in any project. That covers the terminal, the IDE extensions\nand the Desktop app's local Code sessions. Setup can use Desktop's bundled Code executable when no\nterminal `claude` command is installed. Desktop-only users should open a local Code session once so\nDesktop downloads that executable. Setup checks it through the connector's own readiness probe;\nmanaged `spawn` still requires the terminal `claude` command. On a mesh you run with `cotal up`,\nsessions join through `cotal spawn`. A session you open yourself can join only a remote signed-in\nmesh whose authority offers connection approval: run setup once, reopen Claude and ask it to\nconnect (see [personal sessions](#sessions-cotal-did-not-launch)). No MCP\nconfiguration edits or special launch flags are needed for this native plugin path. It also shares your own MCP servers with spawned sessions on its first run (see\n[Sharing your MCP servers](#sharing-your-mcp-servers)), and seeds one `default` persona; `cotal up` brings up the local stack so\n`cotal spawn --detach` / `cotal_spawn` work right away. Re-running either is idempotent.\nA repeat `cotal setup` and `cotal update` refresh the copy under `~/.cotal/claude-plugin` and update\nexisting Claude plugin registrations, including older cached installations. After an npm upgrade,\nthe first regular Cotal command also refreshes these copies before accepting the new generation. Setup,\nskills installation and refresh share a machine-local lock through the final plugin check, so simultaneous\ncommands cannot overwrite a generation while another command verifies it. Disabled installations\nstay disabled during an update. Claude Code can apply an update run in a project folder to an\nenclosing project that has its own installation, so the folder's local or project installation stays\nold. That installation does not stop the refresh or later commands: Cotal prints a warning naming the\nfolder and the `claude plugin uninstall` command to run there to remove it. A user-scope installation\nthat does not update is still an error. `cotal status` checks the resolved payload's manifest version and\nfile contents. A folder-backed plugin can retain older version metadata in Claude's installation\nrecords; that metadata does not override the files Claude loads. Running sessions keep the\ncode they loaded; reload plugins or reopen Claude to load the update.\nThe install mechanics and the invariants behind them are in\n[setup internals](setup-internals.md).\n\n`cotal setup` also installs Cotal's authored Agent Skills (`SKILL.md`, the agentskills.io format) for\ncoordinating agent teams (today `team-topology`), from one canonical source, on two channels:\n\n- **Claude Code** gets a second, skills-only plugin, `cotal-skills`, from the same `cotal-mesh`\n marketplace, at **user scope** (machine-wide). The Claude connector declares and implements this\n setup provider, including the marketplace assets and native plugin commands; the base CLI only passes\n the vendor-neutral Agent Skills directory. The plugin carries no code and no core dependency,\n and uninstalls on its own with `claude plugin uninstall cotal-skills --scope user`. Its plugin version\n is stamped from the running CLI release. `cotal update` refreshes an existing skills plugin without\n enabling it or installing a removed one; `cotal setup --skills` also installs missing skills. `cotal setup` installs it on first run and on repeat\n runs, so upgraders are not left behind. The same provider reports the plugin and skills plugin rows\n in `cotal status`, which point a stale or missing skills plugin at `cotal setup --skills`.\n- **Every other harness** (Codex, Cursor, OpenCode, Gemini CLI, Windsurf/Devin) reads the cross-vendor\n `~/.agents/skills/` directory convention, which has no remote index, so `cotal setup` **reconciles** it\n (and `cotal setup --skills` does only that):\n it installs/updates each Cotal skill, backs up a copy you have edited to `SKILL.md.bak` before\n replacing it, and removes a Cotal skill that is no longer shipped. Only skills Cotal owns are touched;\n your own or third-party skills there are left alone. `cotal status` reports whether the drop is current,\n stale, missing, or has a retired skill to reconcile, and names `cotal setup --skills` as the remedy. This is the working cross-vendor path.\n\nCotal also generates an [Agent Skills discovery index](https://cotal.ai/.well-known/agent-skills/index.json)\non cotal.ai, but that RFC is still a draft with no harness consuming it yet, so it is a forward bet,\nnot a channel to rely on today.\n\n## Enable channels for your organization\n\nFor claude.ai Team and Enterprise organizations, channels are blocked by default. An organization\nOwner must enable them in [Organization settings > Claude Code > Channels](https://claude.ai/admin-settings/claude-code),\nor set this organization-managed setting:\n\n```json\n{\n \"channelsEnabled\": true\n}\n```\n\nUsers cannot override this setting through local or project settings. Cotal's\n`--dangerously-load-development-channels` launch flag cannot override it either. Enable channels\nbefore launching the sessions that need them.\n\nWhen channels are blocked, Cotal's MCP tools and hook-based inbox delivery still work when the agent\ntakes a turn. An incoming message cannot wake an idle Claude Code session. A successful spawn or a\nconnected MCP server does not prove idle wake is available; check Claude's startup notice before\nleaving agents unattended.\n\nPersonal Pro and Max accounts without an organization do not need this admin opt-in. Sessions still\nneed to opt into their channel, which Cotal's managed launch does for its own server. See\n[Anthropic's channel controls](https://code.claude.com/docs/en/channels#enterprise-controls) for\norganization policy and other authentication modes.\n\n## Spawn a session\n\n```bash\ncotal spawn # foreground: your default agent, in this terminal\ncotal spawn dave --detach # supervised: the manager runs it in a PTY\n```\n\nA spawn resolves a persona from `.cotal/agents/<name>.md` ([agent files](agent-files.md));\n`--model`, `--variant`, `--cwd`, `--prompt`, ACL overrides, and `--share-tools` apply to\nboth forms ([run a mesh](run-a-mesh.md) has the full resolution rules). The session joins\nwith identity from its environment and auto-registers presence by the time it is\ninteractive.\n\nInside the session, the agent orients with one read-only tool, `cotal_orientation`: its\nidentity, the channels it reads and may post to, its capabilities, the tools available,\nwho's present, and unread counts. The full tool surface is the\n[MCP tool catalog](mcp-tools.md). In auth mode the team-supervision tools\n(`cotal_spawn` / `cotal_persona` / `cotal_personas`) are injected **only** for personas declaring\n`capabilities: [spawn]` (the same grant that opens the privileged control subject), so an\nagent's toolset matches its declared capabilities. `cotal_run` is gated separately by\n`run`; use `capabilities: [spawn, run]` for both. Fresh setup defaults include both.\nSee [workflow tool setup](workflows.md#from-an-agent-session) for a first run and missing-tool checks.\nClearing retained history is\noperator-only ([run a mesh](run-a-mesh.md)), never an agent tool.\n\n## How it binds\n\nClaude Code exposes four integration surfaces, and three of them collapse into a single\ndual-purpose MCP server:\n\n| Surface | Mechanism |\n|---|---|\n| Outbound, ambient | command lifecycle hooks (`node dist/hook.cjs`) relayed to the connector over its authenticated local control socket (presence, activity) |\n| Outbound, deliberate | MCP tools `cotal_send` / `cotal_dm` / `cotal_anycast` (+ `cotal_feedback`) |\n| Inbound, pull | MCP tool `cotal_inbox` (same server) |\n| Inbound, push | Channel nudge + hook drain (below) |\n\nThe manager launches the *real* `claude` (no wrapper):\n\n```\nclaude --mcp-config '{\"mcpServers\":{\"cotal\":{\u2026}}}' \\\n --dangerously-load-development-channels server:cotal \\\n --permission-mode bypassPermissions\n# env: COTAL_SPACE, COTAL_NAME, COTAL_ROLE, COTAL_CHANNEL=1, plus claude's documented auth vars\n```\n\n- **Tool permissions.** A managed seat has no human at its terminal, so it launches with\n `--permission-mode bypassPermissions`: a prompt under the default mode would stop its first mesh\n tool call until someone answered it, and nobody can. To get prompts back, pass\n `--opt permission-mode=default` (or another mode); the connector adds the default only when the\n launch options carry neither `permission-mode` nor `dangerously-skip-permissions`. Claude refuses\n the bypass mode as root and under a policy that disables it; such a host must pass a mode.\n\n- **Model auth.** Locally, `claude` still reads macOS Keychain / `~/.claude`. In a container or\n CI there is no Keychain, so the connector forwards the documented credential set:\n `CLAUDE_CODE_OAUTH_TOKEN` (from `claude setup-token`), `ANTHROPIC_API_KEY` /\n `ANTHROPIC_AUTH_TOKEN`, and the cloud-provider flags plus their credential vars. Host-session\n markers (`CLAUDE_CODE_CHILD_SESSION`, `CLAUDECODE`) stay out so a nested seat still saves a\n transcript. See [Deploy](deploy.md).\n- **Persona privacy.** The persona body is written to a private file and Claude receives only\n `--append-system-prompt-file <path>`. The body never appears in the spawned process argv. The\n carrier is a 0600 file inside a 0700 directory on POSIX, with equivalent owner-only ACL hardening\n on Windows. That is OS-user isolation: any process running as your user can read it while it\n exists. The manager or the foreground `cotal spawn` removes it, and the shared-server MCP config\n file, once it has proved the `claude` process gone. If the launcher is killed first, a watcher\n started beside `claude` removes them when `claude` exits.\n- **MCP servers.** A spawned agent loads Claude's native MCP sources alongside Cotal and declared\n sharing overrides. Explicit `--share-tools` selections and carried private homes keep strict\n isolation. Claude still applies its own server approvals, disabled settings and managed policy\n (see below).\n- **Lean MCP server.** Every session runs its own cotal MCP server, so Node starts it with\n `--optimize-for-size --max-semi-space-size=1` to shrink its memory footprint. Managed launches and\n the plugin's `.mcp.json` (personal plugin sessions) both pass these flags. `--lite-mode` and\n `--jitless` would save more, but they remove `WebAssembly`, which Node's built-in `fetch` needs,\n so WebAssembly stays enabled.\n- **Light hook.** Claude Code starts the plugin's hook as a new process for every tool call, prompt\n and stop. The hook imports `@cotal-ai/core` only through small subpaths such as\n `@cotal-ai/core/peer-label`, never the package root, which would bundle YAML and NATS into it.\n `pnpm smoke:claude-hook-weight` holds that.\n- **Installed plugin.** The plugin is installed once, at user scope (`claude plugin install\n cotal@cotal-mesh --scope user`), because its hooks bind only to an *installed* plugin. Earlier\n releases installed it per project, at local scope; the next `cotal setup` removes those installs and\n keeps the user-scope one. If Claude Code cannot change a project's settings, setup still removes\n the others, then fails naming that project and the `claude plugin uninstall` command to run there. A session Cotal launches loads the package's own copy with `--plugin-dir`,\n which replaces the installed one for that session, so it still runs one cotal server.\n The repo's `.claude-plugin/marketplace.json` lists the committed plugin tree under\n `claude-plugin/`, which each release regenerates with the built bundles, the skills and the\n release version, so an install from the repo or from a pinned commit runs without a build\n ([Release](release.md)). `cotal setup` (npx, no clone) materializes the same marketplace under\n `~/.cotal/claude-plugin/` from the installed CLI (each plugin dir is rebuilt from scratch and\n atomically replaced, never merged, so no stale file rides in). The\n `cotal-skills` plugin installs from that same marketplace at user scope (`claude plugin install\n cotal-skills@cotal-mesh --scope user`); its manifest and install behavior ship inside the Claude connector, and\n its version tracks the CLI release so updates land.\n- **Identity-gated.** A launched session joins under the identity in `COTAL_NAME`, `COTAL_LINK`\n or `COTAL_AGENT_FILE`. A plain `claude` with none of them joins only when its conversation calls\n `cotal_connect`, so your own sessions do not appear as stray peers. Until then its MCP\n server builds no mesh agent and opens no broker connection. See\n [Sessions Cotal did not launch](#sessions-cotal-did-not-launch).\n- **Hands-free.** The dev-channels flag prints a one-time confirm prompt. The runtime waits for the\n dialog title in normalized terminal text and presses Enter once when it appears, so startup speed\n does not affect a supervised launch. Claude skips that prompt when channels are off for the\n session, for example under a Team or Enterprise organization whose managed settings do not set\n `channelsEnabled: true`, and opens its input screen instead. The runtime then ends its watch\n without a key once the input screen's footer shows `(shift+tab to cycle)` or `? for shortcuts` on\n two screen reads in a row. That seat gets mesh messages through its hooks only, so a message does\n not wake it while it is idle. The PTY runtimes read the child's screen, and the tmux, cmux, Orca\n and Herdr runtimes read the pane's screen. If neither the prompt nor the input screen appears\n within 15 seconds, the seat ends with a bounded error naming the unmatched prompt instead of\n hanging silently or answering another dialog. The PTY runtime writes that error to the seat's output and\n stops the child as a graceful stop does, so a child that ignores SIGTERM is killed 3 seconds\n later. Confirmation ends before the stop begins, so later child output receives no startup\n response. The other runtimes write the error to the manager's log.\n- **Trusted directory.** An authorized supervised spawn accepts the selected workspace through\n Claude's native trust dialog, which Claude draws in one of two layouts. When a numbered list\n shows Yes first and selected, the runtime confirms it with Enter. When No comes first and is\n selected, the runtime moves to Yes and confirms only after seeing Yes selected. It never confirms\n while No is selected, sends each response once, and stops watching after the\n development-channel confirmation or on the input screen. A changed or unsupported dialog receives no guessed input\n and still fails within the startup bound. Claude saves trust with its own repository and linked\n worktree rules; Cotal does not rewrite the manager's configuration. Accepting trust enables the\n workspace's hooks and settings, which can select a tool permission mode when no launch flag\n overrides it. Cotal adds no tool-permission bypass flag. Review the checkout before authorizing\n a spawn there. A foreground `cotal spawn` keeps the trust dialog in your terminal.\n\nInbound mesh messages arrive in context as\n`<channel source=\"cotal\" from=\"bob\" kind=\"dm\" \u2026>\u2026</channel>`: each meta key a tag\nattribute the agent can read for routing.\n\nThe consuming connector holds a crash-safe claim for its broker target, space, principal and\nlifecycle on this OS user account. A second patched session on the same host refuses before\npulling that inbox, even when its launch files and control socket differ. Reconnect keeps the\nclaim. Hooks and non-consuming helper connections do not claim it. The control listener binds\nbefore the broker connection and never removes an existing socket path.\n\nThis is cooperative same-host containment. Other hosts, broker URL aliases and older clients\nare not fenced. Preserve the existing session before choosing a distinct actor for a new\nlaunch. An uncertain shutdown keeps custody until the consuming process exits.\n\n## Sessions Cotal did not launch\n\nA Claude Code session you start yourself, where the `cotal` plugin is installed, gets the tools the\nhosted connector offers: `cotal_connect`, `cotal_wait`, `cotal_disconnect` and the routed mesh tools.\nNothing joins until the conversation calls `cotal_connect` with a name. The plugin's server then runs\n`cotal app-identity provision` with the CLI that `cotal setup` recorded beside the plugin\n(`dist/cli.json`), so it does not need `cotal` on `PATH`. Each conversation gets an identity of its\nown once a person approves it. To choose a registered mesh for this conversation, pass `space`:\n\n```json\n{\"name\":\"my-session\",\"space\":\"cotal_cloud\"}\n```\n\nThis is the native plugin's `cotal_connect` argument. It takes precedence over the saved selection\nand the project folder, without changing either. `cotal space` lists this machine's registered\nspaces. A blank or unknown space is refused; it is never replaced by another mesh. With no `space`,\nthe plugin uses the mesh a bare `cotal spawn` from the session's directory would pick, following\n[the CLI's selection rules](cli.md#selecting-a-space). Do not rename `.cotal` directories or add\n`COTAL_SPACE` to choose a personal conversation's target.\n\nA connected conversation must call `cotal_disconnect` before choosing another space. If its request\nis still awaiting approval, choosing another space first finishes recovery and retirement of the\nprevious request, then asks for a new approval. An unconfirmed retirement blocks the switch and keeps\nthe old recovery record. A second host cannot open the same native session on another mesh while\nits original consumer is still alive.\n\nThe hosted HTTPS connector chooses its space during browser authorization. Its OAuth grant is\nbound to that space; a native `space` argument cannot retarget a hosted grant.\n\nA static-auth or open mesh has no page where a person approves a connection, so the plugin\nopens no identity there. A plugin that `cotal setup` did not install has no record, and\n`cotal_connect` names that remedy.\n\nPersonal native Code sessions can join a signed-in mesh whose authority offers connection approval.\nThe plugin binds the real native session and transcript before requesting an identity. Each\nconversation gets a separate actor. A new identity is issued only after a person approves it in the\nauthority's signed-in web page. A model's tool arguments, a client flag or a native tool approval\nnever approve it. The request first comes back pending with the page's address and its expiry, and\nnothing is issued yet. Connecting again sends the same request and secret: while it is pending it\nanswers the same page, and once approved it issues the identity. A denial or an expiry drops the\nrequest, and the next connection asks again. No login proof reaches the conversation or its tools.\n\nWhile a bound Code conversation's request is pending, the plugin's server replays that same request\nto the authority every ten seconds, serialized with the conversation's own `cotal_connect`, until\nthe authority records a decision, the request passes its expiry, or the session ends. The server\nissues nothing by itself: an approved replay receives the identity the authority issued for that\nrequest, and the server holds it, unclaimed and unconnected, for the conversation's own\n`cotal_connect` with the same name and the `approval_id` from its pending result. The decision\nreaches only the native session that asked, through its wake reader, as one fixed line: approved\n(naming the `approval_id` to finish with), denied, or expired. Nothing else is written that way, and\nthe native module admits no other shape. A session that ends before claiming an approved identity\nhas it retired; a request still pending when the session ends stays saved, and the CLI\nfinishes it before that conversation's next request. An `approval_id` that is not this\nconversation's own pending request is refused before any request is sent. Desktop chat has no\nnative reader, so there the person asks Claude to retry.\nA remote mesh must advertise conversation identity v1 and connection approval v1 on its pinned HTTPS\ndiscovery endpoint. An absent capability, a different origin or a changed registration is refused\nbefore authenticated issuance, for a request without a selection too. There is no immediate-grant\nfallback. A signed-in mesh whose authority this machine holds has no approval page, so it issues no\nnew identity either.\n\nEvery identity sends and receives direct messages and publishes its own activity on its own event\nchannel. These come with the identity and are not optional, so the approval page shows them before\nthe person decides. Native activity sharing includes the messages the person types, assistant\nreplies, and each tool call's name with its start and end, after activation. It is not only presence\nor status. Tool arguments and tool results are withheld from the event channel, as on every connector\n([Event plane](#event-plane)). While a tool waits for permission, presence names it with a short\ndetail such as its command, and every roster reader sees that. Reasoning is not shared by default,\nand earlier or pre-approval history is not shared. The space's activity read permissions determine\nwho can read the event channel. Decline the connection if you do not want this sharing.\nA request without a selection proposes nothing beyond these rights: no role,\ncapabilities or ordinary channel reads.\n\n`cotal_connect` in a native Code session also takes a selection: `channels` to read\n(`access: \"read\"`) or read and post to (`\"read-post\"`), a `role`, and `capabilities` (`spawn`,\n`run`). The selection is a proposal. The person may approve it, change it within their own current\naccess, or approve nothing beyond the identity's own permissions. The issued grant must match what\nthe person approved item for item, or the connection is refused. When the authority refuses an item\noutside your own grant before asking anyone, nothing is issued and the reply names every refused\nitem. There is no fallback to the event-only identity. Its own event channel stays the first post\nentry. A remote mesh must also advertise `conversationIdentitySelection` version 1, or a selection\nis refused before any request.\nEndpoint calls are not selectable through this connection approval contract, which accepts only\nan empty endpoint selection. These conversations do not offer `cotal_describe` or `cotal_invoke`.\n\nA connection is offered the tools its grant can use: `cotal_spawn`, `cotal_persona`,\n`cotal_personas` and `cotal_despawn` with `spawn`, `cotal_run` with `run`, and `cotal_reconnect`.\nIts manager calls exchange the conversation's own secret for a manager-call view, so they carry only\nits grant and name the manager instance when more than one could answer. A conversation is not a\nmanaged seat. `cotal_despawn` without a name refuses rather than stopping your app, and\n`cotal_disconnect` retires only that connection. No run turn is addressed to it, because a workflow\naddresses turns to agents it spawned, so `cotal_yield` is not offered. Persona text and model pins\ndo not change the conversation's own instructions or model.\n\nThe plugin's ordinary command hooks supply the session and transcript binding. Its native module\nuses that binding without intercepting `classic.*` events, which Claude's built-in security guard\nwithholds from user plugins. The guard stays enabled. If an organization disables command hooks or\nnative modules, its administrator must permit them before this connection path can work. Command\nhooks still report lifecycle status and activity; the native module owns incoming message delivery.\n\nA signed-in identity keeps its request and secret in private local recovery state. A lost create\nresponse retries the same request without rotating its secret or allocating another identity.\nRetiring a request that is still pending drops it, and no identity is issued for it. An identity\nissued before connection approval keeps working, and its recovery stays exact. Another plugin server reuses that identity only after the process that held it no longer exists.\n`cotal_disconnect`, session end and a signal request retirement of that lifecycle only. Other\nconversations stay connected. When the mesh reports retirement pending or does not confirm it, the\nconversation stays disconnected and keeps its saved record. Every tool then reports the same\nunfinished cleanup with the mesh's retirement phase. Calling `cotal_disconnect` again retries it at\nonce. `cotal_connect` retries it too, and connects with a new identity once the mesh confirms\nretirement. A restarted plugin server recovers the same way. After each connection, the plugin\nretires the same mesh's saved identities whose plugin server process no longer exists, such as one\nkilled before it could retire them. This runs in the background, so the connection does not wait\nfor it. A server process that still exists keeps its identity, and a retirement that does not finish\nstays saved for a later run. A mesh requiring events refuses a connection without a native\ntranscript source.\n\nThe gateway's MCP annotations distinguish read-only queries from updates. Orientation, connection\nstatus, the roster, the docs and channel information are read-only. Sending messages, connecting and joining\nchannels are additive. Inbox reads and waits consume messages; status, channel mode, leaving and\ndisconnecting can replace or remove state. These hints do not grant access or override Claude's\npermission settings or an organization's approval policy.\n\nA message for a connected conversation starts a turn through the plugin's\n[native hooks module](https://code.claude.com/docs/en/plugins/mods/overview). This works in supported\nClaude Code hosts, including local sessions in the Desktop app's Code tab. The module runs a local\nwake reader as a child of the same host that runs the MCP server. Each server uses its own socket\nin a private directory. Short messages arrive in full with their sender, message ID and reply\ncontext. Claude can answer these without calling `cotal_inbox`. The module uses Claude's\nplugin-sender attribution, never the person's own identity. While Claude is busy, delivery happens\nat a main-conversation tool boundary before the next request. A request already being sent has a\nfixed message snapshot. A later arrival waits for another tool boundary or starts its own attributed\nturn when the current turn ends; storing a note alone does not count as delivery to that request.\n\nEach inline batch carries at most eight whole messages. Each rendered message must fit within\n1,024 Unicode code points, and the encoded socket frame must fit within 4,096 characters. Larger\nmessages remain queued. A notification-only reader or an oversized message uses\n`Cotal: a new message for connection_id <id>. Read it with cotal_inbox.` and a bounded preview.\nA preview is incomplete; Claude reads its contents through the inbox. An inline batch also names\nhow many other messages remain pending.\n\nThe transcript shows a compact Cotal indicator. Expand it to inspect the delivery or preview.\nThis changes only the display. Presentation observes only prompt rows; in-turn appends use their\nreturned row ID, without observing protected notes or skill output.\n\nSocket writes and previews do not acknowledge messages. A complete inline batch is committed only\nafter the native host confirms admission to the bound conversation. An idle prompt also needs its\nstored prompt row; a busy append needs its returned row ID and complete stored text. An offer waits\nfor a busy conversation's next safe boundary while its reader remains connected. Admission after\nclaim has its own deadline. This confirms host handoff, not that\nClaude understood or completed the work. Delivery remains at least once after an unknown result.\nMessage IDs distinguish retries from new requests.\n\nA wake follows the rule `cotal_wait` follows and does not compete with an active wait. Confirmed\ninline delivery rearms the next wake without an inbox call. A replacement reader reoffers unread\nwork. An offer from a closed connection cannot suppress its replacement connection's messages.\nClaude's Stop and organization controls still apply. Refused delivery stays pending without\na retry loop; genuine person-origin input resumes it with a fresh offer. `cotal_connect` reports\nwhether the incoming reader is attached; attachment alone does not prove native admission.\nWhen native modules are unavailable or blocked, the conversation can listen with `cotal_wait`.\n\nClaude Desktop chat does not run a plugin's local server. On macOS and Windows, where Claude Desktop\nis installed, `cotal setup` adds a `cotal` server to Desktop's own config,\n`claude_desktop_config.json` (Settings > Developer > Edit Config), and keeps every other entry there.\nIt runs the same server with `--desktop`, by the absolute paths of the node that runs the CLI and of\nthe plugin copy under `~/.cotal/claude-plugin`. Quit and reopen Claude Desktop to load it. A repeat\n`cotal setup` and `cotal update` keep the entry current, `cotal status` reports it, and a config\nfile that is not valid JSON is refused and left as it was. Desktop reads that file when it starts\nand writes the copy it holds back while you use it, so an entry added while it runs is lost. On\nmacOS, setup and update therefore leave the file alone while Claude Desktop is running, finish the\nrest of their work, and say so. Quit Claude Desktop, then run `cotal setup` again from a terminal\noutside it; a terminal in its Code tab keeps Desktop running. On Windows they still write while\nDesktop runs, because the loss was not reproduced there. Setup can use Desktop's bundled Code\nexecutable when there is no standalone `claude` CLI. Its installed MCP and command hooks use the\nrecorded Node executable, so the app need not find Node on `PATH`. On Linux, setup writes nothing\nfor Desktop Chat.\n\nDesktop Chat supplies no native Code transcript binding, so this local server cannot join a\nsigned-in space, such as one on Cotal Cloud, and refuses before identity issuance. Desktop Chat joins\na signed-in space only through the hosted connector that the space's discovery document advertises\nas `cloudCapabilities.hostedConnector`, version 1 with an HTTPS URL. Add that URL in Claude under\nSettings > Connectors > Add custom connector. Setup for a signed-in space writes no Desktop entry and\nprints the URL instead. `cotal status` prints it on the Claude Desktop row when the selected space is\nsigned in, and the refusal names it. When the space advertises none, all three say so. The Desktop\nserver's instructions tell the model to use the hosted connector for a signed-in space. The hosted\nconnector is a separate connection path; it does not supply autonomous chat wake.\n\nDesktop's working directory is not your project, so its mesh is the `cotal use` mesh, else the only\none running. Desktop runs no monitor, so a Desktop conversation hears messages when it calls a tool,\nand keeps calling `cotal_wait` when you ask it to keep listening. A chat with no turn running cannot\nbe woken by any server: Claude's chat surfaces start a turn only from the person.\n\n## How messages reach the session\n\nDurable deliveries land in the connector's inbox from JetStream consumers\n([SPEC \xA78](../SPEC.md#8-nats--jetstream-binding)); live channel traffic can instead arrive\nthrough an at-most-once core subscription. A durable message sent while the agent is busy\nor offline waits on the stream. Two things move a message from inbox to model; one\ndelivers, the other only wakes:\n\n- **Hook drain (delivery).** `SessionStart` / `UserPromptSubmit` hooks read automatic inbox items and\n inject them as `additionalContext`. This is the single authoritative path: deterministic and works\n on any Claude Code build. Quiet ambient is excluded and stays buffered for `cotal_inbox`.\n A message is **acked only once the hook reply carrying it has cleared both legs of its journey**:\n the connector's control socket to the hook process (which gives up after 2s), and the hook\n process's own stdout to Claude Code (which it force-exits 1s after starting to write). The relay\n sends a receipt back down the control socket from that stdout write's callback, and only on a\n clean write (a runtime whose pipe has gone away fails it), and the connector treats that receipt,\n not its own socket write, as delivery. So a large injection killed mid-flush, or one written to a\n broken pipe, leaves the message un-acked and JetStream redelivers it. What this does *not* prove is\n that Claude Code read or applied the reply: a payload small enough to fit the pipe buffer is\n reported written the moment the kernel takes it. That residual is why the path errs toward\n at-least-once rather than treating a confirmed write as a confirmed read. Acking when\n the reply was merely *formatted* meant a lost reply was a lost message: it was already marked\n handled, so its own redelivery was silently acked on arrival.\n A hook whose handler throws still returns an empty reply so the session is never blocked, and\n that reply carries nothing, so it commits nothing: the batch it had started to surface stays\n un-acked and goes out on a later frame. A manager-launched seat pulls its run turns\n (`turn-pending`) when its manager's turn notice arrives, when it connects, and after a reconnect\n while it holds a turn. The manager repeats a notice until the seat pulls the new turn, so an idle\n seat sends nothing. A pull that fails because the manager is down or unanswered is\n retried once every 15 seconds until one succeeds; a refusal is logged and not retried. The seat\n also drops any `turn-pending` row that breaks\n the manager contract, such as one with no integer deadline, and says so once in its log. A reply\n with no `turns` array changes nothing: the seat keeps the turns it already holds.\n This errs toward **at-least-once**: if a reply lands but its confirmation does not, the batch is\n surfaced again and flagged as a possible repeat. A duplicate injection is noise; a buried DM stops\n the peer answering at all.\n- **Channel nudge (wake).** An arriving message fires a `notifications/claude/channel`\n event that wakes an *idle* session into a turn, so the drain runs *now* instead of at\n the next prompt. The nudge never acks anything. A nudge that the host rejects is retried with a\n bounded backoff while anything is still pending. For an idle session it is the only wake source,\n so dropping it means silence until someone types. When the channel becomes active, the connector\n first re-fires a focus mention remembered during startup, otherwise one buffered wake. A rejected\n push keeps its bounded retry, and JetStream redelivery remains the durable backstop for unacked\n inbox items. Neither a redelivery nor that retry repeats a nudge already pushed for that message,\n whether the message had its own nudge or was counted in a batch one, so a session held in a long\n tool call gets one nudge per message. Once a hook frame carries the message, or the push that\n announced it fails, its next redelivery nudges again, so a reply that never reached Claude Code\n still recovers. If the channel cannot run at all, delivery still waits for the next hook. Live-only\n traffic has no durable retry.\n\n**Two priority tiers.** A *directed* message (DM, anycast, or a channel message that\n`@mentions` us) always nudges. *Ambient* channel chatter does not nudge mid-turn; it\naccumulates, and the `Stop` \u2192 idle transition fires one batch nudge so the backlog drains\ntogether.\n\n**Constraints (accepted).** Channels are a Claude Code research preview (\u2265 v2.1.80;\npermission relay \u2265 v2.1.81): Anthropic auth only, admin-enabled on Team/Enterprise, and a\ncustom channel needs the `--dangerously-load-development-channels` launch flag. The hook\ndrain does not depend on any of that; the channel only adds \"wake me when idle.\"\n\nThe same channel also relays **tool-permission requests** onto the mesh, so a peer (a\nhuman at the CLI, a policy node) can approve or deny an agent's pending tool call through\nCotal rather than a per-terminal prompt.\n\n### Attention\n\nAn agent picks how aggressively peer traffic reaches it with\n`cotal_status({ attention })` (three modes, orthogonal to presence):\n\n| arrival | open (default) | dnd | focus |\n|---|---|---|---|\n| directed (dm / anycast) | wake + inject | wake + inject | wake + inject |\n| channel `@mention` | wake + inject | wake + inject | ack-drop; wake to *pull*; not injected |\n| ambient channel chatter | wake when idle; hold while working | never wakes; injects next turn | ack-drop; recall via `cotal_inbox` |\n\nPer-channel overrides refine this: **quiet** (delivered, never wakes; `@mention` still\nwakes) and **muted** (dropped on receive, mentions included; DMs/anycast unaffected), set\nwith `cotal_channel_mode` or as agent-file defaults (`quiet:` / `muted:`,\n[agent files](agent-files.md)). A per-channel override is the final word for that channel.\nQuiet ambient is pull-only: it never hitchhikes on a human prompt, DM, mention, or other\nconnector-driven turn. `cotal_inbox` explicitly surfaces and clears it. A quiet-channel\n`@mention` remains automatic and injects normally.\n\nA pull is bounded too, and clears only what it hands over. One `cotal_inbox` call carries at most a\nreceivable window (direct messages and role requests first, then channel traffic, replayed history\nlast); whatever does not fit stays buffered, is named in the reply, and comes back on the next call.\nA message too large for one whole response is delivered in parts: once no smaller mail is waiting,\neach call carries its next part, and it is cleared only after its last part goes out, because clearing\nwhat was not handed over is the loss this bound exists to stop.\nThat matters most on the path where it is easiest to lose mail: reconnecting brings a channel-history\nreplay with it, so the largest payload and the least expendable message arrive in the same read.\n\nThe local inbox is bounded. On pathological overflow it evicts pull-only items first, then other\nchannel traffic, and a direct message or role request only when the whole buffer is directed mail.\nIt never evicts a message the connector is handing to the session or a turn is still carrying.\nAn evicted channel item is acknowledged. An evicted direct message or role request never is, and\nthe broker redelivers it after the ack wait until a redelivery finds room. A direct message stays\npending on the session's DM durable, where `cotal deliver pending <name>` counts it. A role request\nstays on its role's shared queue, which that command does not read. A full inbox therefore delays\ndirected mail until the session drains it.\nIf the bounded live/durable classification guard also fills, the connector fails closed:\notherwise-normal ambient becomes pull-only until restart. Muted hard-drop and normal focus recall\nstill take precedence. Focus also keeps a bounded exclusion list so mode toggles cannot recall\nquiet/muted traffic; if that safety bound fills, recall skips the affected channel and reports it\nas incomplete rather than risk resurfacing excluded content. Recall cannot tell one message with an\nempty id from an identical one with another disposition, so in focus such a message is held in the\nlocal inbox as pull-only instead of being dropped, and a mention of it still wakes the agent. When\nthe session settles an id-less copy, it reads the chat stream's last sequence. Identical copies\narrive in stream order, and those reads can answer out of order, so the first read that can see the\ncopies binds them in arrival order, latest first, each to the latest unbound stream copy at or below\nthe lowest sequence read for it or any later identical copy.\nWhile the connection stays up, every stream copy at or below that sequence reached the session\nfirst, so a later identical copy sent during a reconnect gap is above it and stays unbound. A copy\nthat arrives while a read runs may not be in that read, so it binds nothing there and recall reads\nthe channel again, up to three times; if it still could be a stream copy in the last read, recall\nleaves that stream copy in the stream and reports the channel as incomplete. A settled copy a\ncomplete read cannot bind is behind the focus start or out of retention, and is forgotten. Recall\nhands back into the inbox only the stream copies nothing is bound to, such as one sent during a\nreconnect gap or one the inbox evicted on overflow, and `cotal_inbox` hands each over once. Overflow\nfrees only the evicted copy, held or quiet, and a copy no read has bound yet keeps its place in\narrival order, so an identical muted copy stays out of recall. When\nthe inbox is full, recall leaves them in the stream for a later call and reports the channel as\nincomplete. A history read that fails, or a channel with replay off, settles nothing and is reported\nas incomplete, and recall calls run one at a time. If the sequence read for a settled copy fails,\nor answers only after the connection dropped, recall skips that channel for the rest of the focus\nperiod and reports it as incomplete. Recall cannot tell a late copy of a message it handed back from\na new identical message, so every copy takes its own disposition: a new identical quiet mention is\nstill delivered automatically, and a late copy can surface a second time.\nA recalled message that already went out in part is read to its last part, even if an exclusion\nlands after its first part. One session reads its inbox one call at a time: a `cotal_inbox` call\nthat overlaps another waits for it to finish, so neither decides from a view the other has already\nmoved past.\nIf the separate hard-drop disposition guard fills, channel traffic is dropped for the rest of the\nsession rather than risk a late copy bypassing an earlier muted/focus decision; DMs and anycast are\nunaffected.\n\nAttention is **advisory UX, not a boundary**: any peer can wake a dnd/focus agent by\nnaming it, and `muted` means \"I opted out of receiving\", not \"the channel is blocked\";\nthe broker still authorizes and delivers. Focus's real effect is shrinking the\nuntrusted-ambient injection surface (only subject-authenticated dm/anycast auto-inject).\nIt resets to **open** on `SessionStart`, so a restarted agent never stays silently deaf.\nYour attention is mirrored into presence so peers can see it.\n\nWhatever does reach a turn is framed so a peer cannot write the frame. A line that begins at column\nzero is written by the connector; one message is one line plus indented continuations, with the\nsender inside a single bracket pair. A message body, a sender name and role, and a service or\nchannel label are all peer-controlled, so each passes through the same neutralization the\n`cotal_inbox` reply uses: no line break a splitter may honour and no bracket survives into a\nrendered attribution. This matters more for an injected block than for a reply, because the agent\ndid not ask for it and so never had the chance to distrust it.\n\n## Presence mapping\n\nThe connector wires a small subset of Claude Code hooks to presence states; presence is\ncoarse, and \"what it is doing\" rides on activity updates. Presence is **advisory**: a presence\npublish that fails (the endpoint mid-reconnect, say) is swallowed and never prevents the same hook\nfrom delivering messages or flushing held ones.\nA `SessionStart` during an open turn, including compaction, preserves the current `working` or\n`waiting` status until `Stop`, `StopFailure`, or `SessionEnd` closes the turn.\n\n| Hook | \u2192 state |\n|---|---|\n| `SessionStart` | `idle` only when no turn is open (join; surfaces the inbox; captures the live model into `meta.model` when no pin) |\n| `UserPromptSubmit` | `working` (turn starts; surfaces the inbox) |\n| `PreToolUse` | no change; records *what* is about to run, so a permission wait can name it |\n| `PostToolBatch` | `working` when the main agent's batch resolves after a prompt left the seat `waiting`, which clears its condition so the turn's `Stop` yields; a subagent's batch (it carries `agent_id`) or one relayed after the turn's terminal changes nothing |\n| `Notification` (`permission_prompt` / `agent_needs_input`) | `waiting` with condition `approval` / `input` (activity leads with the pending tool, e.g. `Bash: git push \u2026`) |\n| `Stop` / `StopFailure` | `idle` (turn done / died on an API error; flushes anything held while busy). `StopFailure` also relays Claude Code's native error value as `condition.source` and maps it to the closed condition vocabulary. On the [event plane](#event-plane) it closes the run with `RUN_ERROR`. |\n| `SessionEnd` | `offline` (graceful leave) |\n\nThe connector also leaves gracefully when its stdin closes. An MCP client closes it to end the\nsession, and a killed `claude` closes it with no `SessionEnd`, so a dead session drops off the\nroster instead of staying on it as a live peer.\n\n`StopFailure` maps `rate_limit` and `overloaded` directly; auth and credential failures to\n`auth`; account and billing failures to `billing`; `invalid_request` to `request`;\n`model_not_found` to `model`; `server_error` to `server`; `max_output_tokens` to `context`; and\n`unknown` to `failed`. The native value remains in `condition.source`.\n\nHooks are relayed over the connector's **authenticated** local control endpoint (per-user\nsocket + per-launch token, constant-time checked), so a local process that finds the path\nstill can't drive presence or stop the agent. Claude Code's own\n[hooks reference](https://code.claude.com/docs/en/hooks) lists every hook event. The hooks the\nplugin registers, with their matchers, are listed in the\n[adapter README](../extensions/connector-claude-code/README.md#claude-code-hooks).\n\n## Event plane\n\nA connected native session publishes a structured account of its activity: the messages a person\ntyped, assistant replies, run boundaries, and each tool call's start and end. Tool arguments and\nresults are withheld (below), and a turn a peer message started opens a run without republishing\nthat message. Reasoning is off by default. Programs can read these events directly. The launcher\nsets `COTAL_EVENTS` by default; pass `--no-events` to opt out on an unrestricted\nspace. A user-auth registration with `policy: { events: \"required\" }` carries `eventsRequired` in the\nprivate launch material, so the connector arms even without `COTAL_EVENTS`; `--no-events` is refused.\nA hand-driven user-mode session may carry the same decision as `COTAL_EVENTS_REQUIRED=1`. Its own\npublish grant must cover `events.<owner>.<actor>` or the connector refuses before joining. A personal\nnative Code connection to a signed-in mesh uses the same recorder and captures its transcript\nboundary after approved issuance, before joining. Earlier history and time spent waiting for\napproval are not published.\nIts native lifecycle drives presence and recording for that session only; a subagent or another\nconversation cannot end its main run. The event channel keeps the space's normal read permissions.\nA personal connection makes manager calls only with its own selected grant.\n\nIf the event plane stops, the seat keeps running and its mesh connection, on every connector and\non every space. The connector log records `AG-UI emitter stopped` with the reason and the delay\nbefore it re-establishes the plane: at once after a failure, then every second while the plane\nstays down, a fixed wait that neither grows nor is capped, so the plane is back within a second\nof the fault clearing. The re-establish\nis a fresh start: it reopens the write-ahead log, settles any frame it left pending, builds the\nmapper from the run the log has open, and reads on from the stored cursor, which is the same\nrecovery a connector restart runs. A failed publish or a failed log write is never retried on the\nemitter it failed on. A turn that ends while the plane is down keeps its terminal, and so does a\nturn whose terminal's own log write failed: once the re-establish has read on, the run the log had\nopen is closed with that terminal, a failure as a failure, rather than left open for the next\nprompt to close as a finish. That holds when the next prompt was already written before the plane\ncame back: the failure is published after the failed turn's own records and before the next run\nopens, in a frame of the failed run, since a run error names no run and the frame is its only\nattribution. Every turn that ends while the plane is down keeps its own terminal. A terminal belongs\nto the last turn the transcript had begun when its hook fired: the connector reports a turn's\nterminal once that turn's records are written and before the next turn's are, so the position the\ntranscript has reached at the hook names the turn, including a turn the connector opens no run for,\nsuch as a task notification, which takes its terminal and publishes nothing. A terminal repeated\nfor a turn that has one replaces nothing and is kept for no later turn. A line the connector cannot\nread, written past that position while the plane was down, does not hold the terminal: the\nre-establish reports the gap and places the terminal after the records before it. A record no read\nanswers, such as an OpenCode part still filling when its turn died, holds it no more: the terminal\nis placed after the records before it, and the turn end reads the session once rather than without end. The OpenCode source reads every record at or before the end it reported at a turn end as settled, so a part left filling by a failed turn does not stop the turns after it from publishing. A terminal reported while the first start is still waiting on the broker reaches the emitter before its first read, so a prompt written in that window does not close the failed turn as a finish. The same placement runs with\nthe plane up: a terminal reported while the flush before it is still reading, with the next prompt\nwritten before that read, closes its own turn in the place of the finish that prompt synthesizes. The seat used to exit on a space that requires events; for Claude Code the\nconnector is the MCP server, so that exit showed as the server disconnecting, and Claude Code\nrestarted it on the next tool call.\n\nA transcript line that is not a record does not stop the plane. Claude writes the transcript, and a\nwrite cut short by a full disk leaves part of one record glued to the next on a single line, which\nnever becomes readable. The connector steps over that line, keeps the records after it, and writes\n`skipped an unreadable transcript record at offset <offset> (<bytes> bytes)` with the parser's\nreason to its stderr.\n\nA new session includes its first run even when Claude writes a positional startup prompt before the\nconnector receives `SessionStart`. That from-zero read is keyed only to Claude's explicit\n`source: \"startup\"`; resumed, forked, cleared, and compacted sessions adopt at the transcript boundary\ncaptured at that adopt, before the mesh link connects, so nothing Claude appends while the connector\nis still starting up lands behind the cursor and is silently dropped. Crash recovery follows the\ncursor already stored in the event write-ahead log, regardless of the new process's startup label.\n\nClaude starts each hook in its own process, so a prompt or stop relay can reach Cotal before the\n`SessionStart` relay. The connector holds those event flushes and the terminal until `SessionStart`\nsupplies the source, then enqueues adopt, flush, and close in that order.\n\n`SessionStart` can also run before the connector process has bound its local control socket. The hook\nthe `SessionStart` relay retries only transient pre-connect listener errors, with capped backoff\ninside its existing two-second budget. Later hooks and permanent local faults still fail open\nimmediately. Once a socket has connected, a broken exchange is not retried: the connector may\nalready have handled the frame, so replaying it could apply one lifecycle event twice.\nThat retained `SessionStart` can itself arrive before Claude creates the transcript path. A genuinely\nnew startup waits up to five seconds for that file with capped backoff, and the same deadline bounds\none stalled file read; expiry fails loud instead of silently losing the first run. A forked session\ngets the same wait, because Claude copies the parent transcript into the fork's own file after the\nhook, and then adopts at the end of that copy. Resumed, cleared and compacted starts and recovered\ncursors still require their existing source at once.\n\nTool arguments (`TOOL_CALL_ARGS`) and tool results (`TOOL_CALL_RESULT`) are not republished\nonto this channel. The durable emitter drops those events before they are written to the\nwrite-ahead log, because this channel's read ACL is not the ACL the tool ran under. Content is\nmandatory on both kinds, so the event is suppressed rather than emptied or replaced with a\nplaceholder. Tool start and end still go out. A restart that finds a pending pre-fix frame\nstill carrying those kinds HALTS rather than republishing it.\n\nThe channel is **`events.<owner>.<actor>`**, named after the session's principal. What the actor\nhalf is depends on the mesh, and the difference matters when you go looking for it: on a static mesh\nit is a key the manager allocated, never the display name, so two live agents sharing a display name\ndo not share a stream; on a user-auth mesh it is the agent's own name, because that is what the\nledger row is keyed on. Spelled out again with both halves below. The launch grants publish rights\non that channel alone. A spawn\nthat asks for a *different* agent's event channel is refused at the door rather than granted, since\nthat channel is that session's event stream. The same rule runs on restart: a manager\nresume document that names another agent's event channel is refused rather than adopted, because the\nmanaged row is re-armed from that document and the credential is re-minted from the row.\n\nThe rule reads a **concrete** channel, two principal tokens and nothing else. A pattern such as\n`events.<owner>.>` is not an event channel to it and passes untouched, governed by ordinary ACL\nauthority: on a user mesh the delegation envelope, on a static mesh the spawning credential itself.\nThat is deliberate, because the pattern is the form an operator writes on purpose for an observer,\nand it is worth knowing rather than assuming the fence is total.\n\nTo let something else read a plane, grant it out of band. The refusal prints the command for the\nmesh it is running on, spelled out in full, and only that one.\n\nOn a **user-auth** mesh:\n\n```bash\ncotal actor grant <reader> --owner <owner> --scope '' --allow-subscribe 'events.<owner>.<actor>' --allow-publish ''\n```\n\nEvery field, deliberately. `actor grant` is an upsert of the whole row, so it refuses a grant that\nleaves off any of the three ACL flags. Only `--full` turns an omitted flag into the wide default\n(`>` read, `>` post, `spawn,role:default` scope), which is the opposite of what a scoped watcher is for.\n\nOn a **static** mesh there is no actor ledger for `actor grant` to write to, and the refusal says\nso; mint the reader instead:\n\n```bash\ncotal auth mint watcher --profile agent --allow-subscribe 'events.<owner>.<actor>' --provision\n```\n\nThe **agent** profile, not the observer one. `mint` reads `--allow-subscribe` only for that\nprofile, and refuses it anywhere else: `--profile observer --allow-subscribe <channel>` exits\nnon-zero and writes no creds file, because the observer profile carries a fixed read set over the\nwhole chat plane, which is the opposite of what a scoped watcher is for. The agent profile also prints the lifecycle uid the\nreader needs, since an authed consuming endpoint refuses to start without one.\n\nOn an **open** mesh there is nothing to grant: the mesh has no credentials and no ACLs, so any peer\nthat lists the channel reads it, and the refusal says so instead of naming a command. The\nown-channel rule still applies there, because a spawn is not the place to hand out a read on\nanother agent's tool inputs and outputs.\n\nTwo things a reader has to do that are not obvious, both on `CotalEndpoint`. It must pass the event\nchannel in `channels`: an endpoint reads the channels it lists, so one constructed without\nthe event channel joins nothing and the frames never arrive. And it reads history with `readHistory(channel)`, the delivery daemon's mediated read, not\n`channelHistory(channel)`: a scoped credential is denied the ad-hoc consumer the direct read\ncreates, by design. `cotal console` and the web console already do both.\n\nThe `<owner>.<actor>` pair is the session's principal. On a user-auth mesh the actor half is the\nagent's own name, so the channel is `events.<your-owner>.<agent-name>`. On a\nstatic mesh the owner half is the literal `local` and the actor is a key the manager allocated, so\nthe channel is `events.local.<key>`; the spawn reply carries that key as `id`. Note\nthat `cotal console` and the web console keep event channels out of their channel lists on purpose,\nsince a plane is a machine feed rather than a conversation; they draw the frames when you open the\nchannel by name.\n\nThe rule governs the manager's doors, which are the ones a caller other than you can reach. A\nforeground `cotal spawn` on your own machine mints from your own signing material, so it can still\ngrant any channel you name: that is the out-of-band grant, not a way around the rule.\n\n**Failed turns publish run errors.** Claude Code decides for itself\nwhether a turn finished or died and fires one of two hooks accordingly, so the connector relays that\ndecision rather than making one of its own: a turn that ended on an API error ends its run with\n`RUN_ERROR` carrying the fixed message `run failed` and no code. Neither the detail Claude Code\nreported nor its error kind is published there: both are upstream values that can echo your prompt or\ntool output, and the events channel has a different read ACL. The error kind still reaches presence\nas the agent's condition (`rate_limit`, `auth`, `billing` and the rest). A turn that ended normally still\nends with a run-finished event carrying no outcome, which says the turn ended and does not claim it\nsucceeded.\n\nEvents are written to a per-session write-ahead log before they are published, so a hook that fires\nafter a restart resumes at the cursor it left rather than replaying or skipping, and a run that was\nopen when the session stopped is closed rather than left dangling.\n\nA delayed initial connection or a broker reconnect pauses event publication without closing MCP.\nThe recorder waits for both the mesh binding and its transport; unpublished records stay in the\ntranscript until the connection recovers. Stopping the connector cancels that wait. A recording error,\nincluding a conflicting transcript or damaged event state, stops the publishing plane and not the\nsession: it is reported with the delay before the next re-establish, on every space, and the seat keeps\nits mesh connection. A second transcript path offered to a bound recorder is refused the same way, and\nthe first path stays bound.\n\nOne channel carries **every session of one agent**, because it is named after the principal and not\nafter the session. Alongside the per-session logs the connector keeps one small record per principal,\nholding the last sequence the broker assigned on that channel, so a new session continues the stream\nits predecessor left instead of starting again from nothing. Both live under the events state root\n(`COTAL_WORKSPACE_ROOT`), and neither is something you edit by hand.\n\nA **missing** record is not a fault: the connector rebuilds it from the session logs beside it,\nwhich is how an agent that was already running before this record existed keeps its stream. That\nrebuild stops if any one of those session logs is damaged. Unreadable, not valid JSON, and written\nfor a different principal all count, and so does a session directory or a log that is a link rather\nthan the real file the connector wrote, or a log that has more than one name. A tip taken from the\nrest would be too low, and it would stop publication later with nothing left to point at the cause.\nThe connector names the file instead, and the only way past it is the directory removal described\nbelow, under the same condition. A record that **disagrees with the broker** is a fault, and the\nconnector stops publishing and says why rather than guessing. A record that **moved while a session\nwas writing to it** is refused the same way: it means something else wrote the principal's record,\nand the connector reports which value it held and which the file holds rather than writing over the\nlater one. There is no command to clear it. The state is the principal's directory under the events\nroot, and clearing it by hand means removing that directory whole: the sequence, the cursor and the\nper-session logs only mean anything together, so removing part of it leaves a state the next start\nrefuses. Removing it is only half a remedy, and the half that comes first is the channel. The\ndirectory is where the agent's memory of the tip lives, not the tip itself, so on a channel that\nstill holds frames the next session opens expecting an empty one and stops on the same\ndisagreement, with the logs a tip could have been rebuilt from now gone. Purge the channel first,\nthen remove the directory.\n\nReading it: `cotal console` and the web console draw event frames directly. A frame carries no text\npart by design, so a surface that renders a message as flat text shows a marker instead of prose.\n\n**On a per-user-auth mesh, the default event plane needs the spawner's grant to cover the channel.** The event\nchannel is added to the child's publish set, and delegation only narrows: an agent may hand down\na subset of what it holds and no more. So a peer-initiated spawn is refused unless the\nspawning identity's own grant already covers the child's event channel. The refusal prints the\nexact `cotal actor grant` command that widens it. An operator launch, whose chain reaches an\nadmin-scoped or roster row, is unaffected. Passing `events: false` is the explicit opt-out.\n\nArming the event plane through a typed spawn request (`manager.spawn` with `events`, including\nthe CLI's `cotal spawn --detach --events`) is allowed on a user mesh when the child runs under\nthe caller's owner. This also satisfies a registration policy that requires the plane. A spawn\nunder another owner still needs the caller's admin tier. Without it, an explicit request or a\nrequired plane is refused before provisioning. A silent non-owner caller in a space without that\npolicy has the plane disarmed with a notice in a successful reply. Later provisioning can still\nrefuse the spawn. The child's own-channel rule and the ledger's delegation envelope still apply.\n\n## Resume a session\n\n`--resume <session-id>` pulls an existing Claude session, its context and transcript,\ninto the mesh. It **forks**: Claude mints a *new* session id from that transcript\n(`--resume <id> --fork-session`), so the meshed agent gets its own session and the\noriginal is untouched.\n\n- `cotal spawn --resume <id>` (foreground) is the primary surface: the transcript is on\n *your* machine, and errors are Claude's own stderr, inline.\n- `--detach --resume <id> --on <instance>` carries a session held on *your* machine to that\n manager instance, which may run on another host. The CLI finds the transcript under your\n Claude config (`~/.claude`, or `$CLAUDE_CONFIG_DIR`), sends it through a JetStream Object\n Store bucket only that instance reads, under a writer credential pinned to that one transcript,\n and prints `carried session <id> to <instance>:\n sha256:<hex>, <sent> of <size> bytes sent in <chunks> chunks`. Without `--agent`, the CLI first asks that\n instance which harness the persona resolves to, so a persona with `agent: claude` is carried even\n when your `COTAL_DEFAULT_AGENT` names another connector. A re-run of the same bytes\n sends nothing, and an interrupted carry continues where it stopped. The seat forks it in a\n private Claude home under the manager's `.cotal/seat-homes/`, which no other seat's Claude\n lists or finds, and which is removed when the seat stops. When Claude starts the fork, the seat\n records the SHA-256 of the transcript it read from its own project; the manager stops a seat\n whose record names other bytes than the carried ones, or that records none within the join\n timeout after it joins, an uncertain launch included, and otherwise shows that record as the\n seat's provenance. `cotal attach` to such a seat names its source after the seat\n name, as `(resumed from <host>:<id>)`. A remote manager receives a carry when its host issues it a\n transfer reader. On a user-auth mesh the CLI exchanges the operator's login for a one-object\n `transfer-writer` view, which needs scope `admin`.\n- A session name in place of an id is refused, listing each session on this host that carries\n that name with its id, SHA-256 and modification time. An id this host does not hold resolves\n against the **manager host's** `~/.claude`, as before.\n- A seat-private home holds no login. The manager host needs `CLAUDE_CODE_OAUTH_TOKEN` (from\n `claude setup-token`), `ANTHROPIC_AUTH_TOKEN`, or a cloud provider selection in its\n environment; `ANTHROPIC_API_KEY` alone is refused. The private home trusts the authorized\n launch directory without changing the manager's home. Claude must be 2.1.234 or later.\n- The manager waits for a real outcome: `\u2713 started` means the agent *joined the mesh*,\n `\u2717 exited on launch` carries Claude's last output and the seat's exit diagnostic, such as a\n startup confirmation that never appeared, and an uncertain launch (~30 s) is reported without\n tearing the agent down.\n- `--opt resume=` with no id passes a bare `--resume`, which opens Claude's session picker, and the\n agent joins the mesh once you pick a session. A managed launch with it is ready once Claude runs:\n the CLI says the agent is waiting for you at Claude's session picker, attaches you to it in a\n terminal, `-d` included, or otherwise prints the attach command, and exits 0. `--opt resume=<id>`\n passes `--resume <id>` and joins as any launch does.\n- Resume is an **operator surface only**, deliberately not exposed on MCP `cotal_spawn`\n (a mesh peer naming host-local transcripts would widen `spawn` into transcript\n disclosure). Claude, Hermes, Jcode and pi support it, each by forking; OpenCode and Codex fail\n loud ([Connectors](connectors.md)).\n- Needs a `claude` new enough for `--resume \u2026 --fork-session` (verified on 2.1.197).\n\n## Sharing your MCP servers\n\nA spawned session loads the MCP servers your normal Claude Code configuration enables on the\nlaunch host: user, local, project, plugin and connected-account sources. Claude handles discovery,\nprecedence, disabled servers, project approval and managed policy. Cotal adds its mesh server\nand any overrides declared in `connectors.claude.mcpServers` ([format](config.md)). It does not\ncopy native server definitions or credentials into the launch file or onto another machine.\nThe selected `CLAUDE_CONFIG_DIR` remains in effect for an ordinary local spawn.\n\nFirst-run setup still seeds eligible user-scope declarations into the Cotal config and preserves\nan existing list. That list is not the source of truth for native discovery. A same-named declared\noverride takes precedence; changing the native definition does not rewrite an override you kept.\nAn empty Cotal sharing map leaves native discovery enabled.\n\nUse `--share-tools tavily,figma` to load only those declared servers plus Cotal, or\n`--share-tools none` for Cotal alone. These explicit selections use `--strict-mcp-config`.\nCarried sessions also stay strict and keep their seat-private home; operator profiles and\nconnected-account credentials are not copied into them. Native managed policy still applies.\n\nThe child environment stays allowlisted. References in declared shared servers and the selected\nuser-owned MCP configuration forward their named variables. Repository and plugin files cannot\nadd ambient secret names merely by referencing them; supply such names through an explicit\nshared declaration or the existing `spawn.env` policy. Native files retain their own configured\nvalues. Documented native MCP switches, including the connected-account opt-out and tool-search\nchoice, keep their explicit values. Cotal does not forward the entire parent environment or force\ntool definitions to load upfront.\n\nSharing a credential grants it to the session, so enable only servers the session may use.\nEach enabled server may start a process per seat; an explicit sharing selection can reduce that\ncost. Native discovery does not change Cotal's mesh identity, hooks or channel wake path.\n\n### One Cotal path per session\n\nA spawned session is on its mesh through the local Cotal server. A connected-account source can\nalso give it a hosted Cotal connector, which would join the same session a second time under\nanother name. The plugin's `PreToolUse` hook refuses every `cotal_*` tool of any server other than\nthe local one in a session Cotal launched, with the reason in the refusal. It decides in the hook\nprocess, so the refusal holds when the control socket is down. A Claude Code session that Cotal did\nnot launch keeps the hosted connector. Where the plugin is installed, its local server's\ninstructions tell the model to use the local path unless you ask for the hosted one.\n\nIn such a session the plugin also keeps each hosted connection with the session that opened it. A\n`PostToolUse` hook on the hosted `cotal_connect` records the returned `connection_id` under the\nplugin's data directory (`CLAUDE_PLUGIN_DATA`), and `cotal_disconnect` releases it. A fork made with\n`--fork-session` inherits that id in its transcript but has a new session id, so its calls with the\nid are refused and it is told to call `cotal_connect`. A resumed session and its subagents keep the\nsession id and pass. An id the plugin never saw opened goes to the hosted connector, which checks\nthe account.\nThe guard runs only where the `cotal` plugin is installed. A Claude Code session with only the\nhosted connector has no hook, so nothing records its connections and its forks are not refused.\n\nA hosted connector cannot start a turn, so a hosted conversation hears a message only when it calls a\ntool. `cotal_wait` holds until a message that would wake the agent arrives and returns it, or returns\nwith none after the service's bound. The conversation calls it again to keep listening. Every other\nhosted tool adds a line to its result when messages are waiting. A conversation with no turn running\nis not woken.\n\n## Host sessions in one process\n\nA host serving many managed seats can run their MCP servers in one process with the subpath\n`@cotal-ai/connector-claude-code/mcp`, which loads with both `import` and `require`:\n\n```ts\nimport { serveClaudeSession } from \"@cotal-ai/connector-claude-code/mcp\";\n\nconst session = await serveClaudeSession({ env, input, output });\nawait session.close();\n```\n\nLoading the module only exports it; it serves its own stdio only when run, as `node dist/mcp.cjs`.\nThe subpath ships CommonJS declarations so TypeScript hosts can use either module format,\nincluding CommonJS consumers with Node16 or Node18 module settings.\nClaude Code pushes `claude/channel` only to a stdio server it launched, so each `claude` still\nlaunches a stdio command, such as a small relay that pipes its stdio to the host.\n\n- **Managed only.** `env` is a complete launch environment, copied when the call starts. It must\n carry a mesh identity and the full control pair, or the call rejects before the session joins the\n mesh. The presence environment label, bearer commands, carried-fork records and feedback contact\n lookup use that copy. Feedback's Git fallback reads the session's global config, including its\n includes, without reading the host repository config. Set `COTAL_FEEDBACK_EMAIL` or pass the\n feedback tool's `email` argument to choose another contact. Include the `PATH`, `HOME` and other OS\n variables subprocesses need; the host's environment is not merged into them. Subprocesses still\n use the host process's working directory. A session's card does not: its `meta.cwd` is the `cwd`\n its own `SessionStart` hook reports on startup, resume or fork, absent until then, and unchanged by\n clear or compact. It is advisory display data that every reader of the space's roster can see. A plain `claude` with no identity is served by `mcp.cjs` run directly. Sharing a process\n does not isolate untrusted tenants or their files.\n- **Streams.** `input` carries JSON-RPC from the client and `output` carries this session's replies\n and `claude/channel` pushes. The caller owns both streams. Input that has emitted `end` or is\n destroyed rejects startup. Buffered unread input remains eligible. When an active session's\n `input` emits `end` or `close`, the session closes, including after a relay connection resets.\n Teardown may pause `input` and removes the transport's error handler. A host that reuses the\n stream must resume it as needed and keep its own stream error handlers.\n- **`fatalBind`.** Off by default: a control socket that cannot be bound rejects the call and the\n host keeps its other sessions. Only a process that is the session turns it on, as `mcp.cjs` does.\n- **Teardown.** `close()` closes the control socket, the wake policy, the AG-UI emitter, the mesh\n endpoint and the MCP server, and is idempotent. It removes its input listeners without closing\n the caller's streams. The returned promise rejects if teardown fails. A startup failure closes\n what was started before the call rejects. A manager shutdown frame or input EOF or closure closes the\n session and then calls `onShutdown(code)` once. A teardown failure is logged and reported with\n code 1, without becoming an unhandled rejection in the host. Close waits for a pending broker\n dial to settle. A dial that fails after shutdown starts is not a teardown failure. The session\n releases local inbox custody only after its connections are proved closed, so the same lifecycle\n can reopen after a successful close. An active duplicate or an unproved close retains custody.\n\n## Feedback\n\n`cotal_feedback` works out of the box: without a key it posts to the public intake at\n`https://cotal.ai/v1/feedback` (needs a contact email: `COTAL_FEEDBACK_EMAIL`, then\n`git config user.email`, else the agent asks). Set `COTAL_FEEDBACK_KEY=fbk_<key>` in a\nbeta tester's environment to route to the keyed intake (`Authorization: Bearer`, identity\nderived from the key); `COTAL_FEEDBACK_URL` overrides either endpoint. The CLI can send\ntoo: `cotal feedback \"<summary>\" [--type bug]`. Each submission carries\n`origin: human | agent`, whether the tester asked, or the agent auto-reported a major\nissue.\n"
|
|
65927
|
+
"body": "# Connect Claude\n\n> **Guide** (informative) \xB7 **For:** operators \xB7 **Prereqs:** [Quickstart](getting-started.md)\n\nThe Claude Code connector turns a real `claude` session into a Cotal mesh peer. A bundled\nplugin inside the session joins NATS, maps lifecycle hooks to presence, and exposes the\nmesh tools. Nothing wraps Claude; it is an ordinary session that happens to be on the\nmesh.\n\nThe shared mesh runtime (agent, `cotal_*` tools, hook relay) lives in\n[`@cotal-ai/connector-core`](../extensions/connector-core); this connector is the thin\nClaude-specific adapter over it. Its lifecycle hook imports the relay from the\n`@cotal-ai/connector-core/relay` subpath, so each hook process loads the relay and its environment\nreaders and none of the NATS client, zod or yaml. Siblings: [OpenCode](connect-opencode.md) (beta),\n[Codex](connect-codex.md) (beta), [Jcode](connect-jcode.md) (beta), [Hermes](connect-hermes.md)\n(alpha), [pi](connect-pi.md) (alpha); the [Connectors](connectors.md) matrix compares them\nfeature-by-feature.\n\n## Choose a connection path\n\nUse one connection path per conversation. The Desktop app's Code tab and its ordinary chat\nare different hosts.\n\n| Where you use Claude | Connection path | How the space is chosen |\n| --- | --- | --- |\n| A Code session started by Cotal | Keep the connection supplied by `cotal spawn` | The spawn's target space |\n| Your own terminal, IDE or Desktop Code session | Native Cotal plugin | The native connection's target; see [personal sessions](#sessions-cotal-did-not-launch) |\n| Claude web or ordinary Desktop chat | Hosted HTTPS connector | The space approved during connector authorization |\n| Your own Code session where you explicitly choose hosted access | Hosted HTTPS connector | The space approved during connector authorization |\n\nThe native plugin can deliver incoming messages through supported Claude Code hooks. Hosted access\ncannot start a turn in an idle conversation. A hosted conversation receives messages while it calls\nmesh tools, including `cotal_wait`. Adding the hosted connector does not add native wake support.\n\n### Connect through the hosted service\n\nFor Cotal Cloud, the connector URL is `https://cloud.cotal.ai/mcp`. Other deployments advertise their\nown URL. In Claude web or Desktop chat, open Settings > Connectors > Add custom connector and enter\nthe URL. If the connector is already listed or required by an installed plugin, connect that entry\nrather than adding a duplicate. Complete authorization in the browser and choose the intended space.\n\nIn a Code session you started yourself, you can instead register and authenticate the HTTP server\nwith Claude Code's CLI:\n\n```bash\nclaude mcp add --transport http --scope user cotal-cloud https://cloud.cotal.ai/mcp\nclaude mcp login cotal-cloud\n```\n\nAuthenticate the existing entry if it is already registered. This CLI configuration does not\ncomplete authentication for Claude Desktop's separate Connectors settings. Where the local Cotal\nplugin is also installed, tell the conversation to use the hosted `cotal-cloud` server explicitly.\nA Code session started by Cotal must keep its managed connection.\n\nAuthorization gives the client access to the connector. Joining a conversation to the mesh is a\nseparate step: ask Claude to call `cotal_connect` with a name, open the returned approval page in\nyour signed-in browser, inspect the space and permissions, and approve if they are right. A pending\nrequest has not joined. In Claude Code with the plugin's native hooks module, the decision reaches\nthat conversation on its own: the plugin's server follows the pending request and, once you decide,\ntells the conversation to finish the connection, or that you denied it or let it expire. In Claude\nDesktop chat, which runs no native module, ask Claude to retry the connection. Confirm the resulting\nconnection with `cotal_orientation` and check that its space is the one you intended.\n\nIf Claude displays a success notification but its connector settings still offer Connect or report\nunfinished setup, do not treat the notification as proof of a working connection. Reopen the\nconnector details and check whether its tools are available. If the conflict remains, report it as\nan incomplete connector setup; it is not evidence that the separate conversation approval succeeded.\nDo not move project or ancestor `.cotal` directories, change mesh credentials, or add `COTAL_SPACE`\nto repair hosted authentication. Hosted access does not select a mesh from those directories.\n\n## Set up\n\n> **Team and Enterprise accounts:** [Enable Claude Code channels](#enable-channels-for-your-organization)\n> before running unattended agents. Channels are blocked by default, so incoming messages cannot\n> wake an idle session until an organization Owner enables them.\n\n```bash\ncotal setup --configure-only # install the plugin and seed one agent without startup\ncotal up # brings up the mesh + delivery daemon + a detached manager\n```\n\n`cotal setup` installs the cotal plugin for your user account, so every Claude Code session on\nthis machine gets the `cotal_*` tools, in any project. That covers the terminal, the IDE extensions\nand the Desktop app's local Code sessions. Setup can use Desktop's bundled Code executable when no\nterminal `claude` command is installed. Desktop-only users should open a local Code session once so\nDesktop downloads that executable. Setup checks it through the connector's own readiness probe;\nmanaged `spawn` still requires the terminal `claude` command. On a mesh you run with `cotal up`,\nsessions join through `cotal spawn`. A session you open yourself can join only a remote signed-in\nmesh whose authority offers connection approval: run setup once, reopen Claude and ask it to\nconnect (see [personal sessions](#sessions-cotal-did-not-launch)). No MCP\nconfiguration edits or special launch flags are needed for this native plugin path. It also shares your own MCP servers with spawned sessions on its first run (see\n[Sharing your MCP servers](#sharing-your-mcp-servers)), and seeds one `default` persona; `cotal up` brings up the local stack so\n`cotal spawn --detach` / `cotal_spawn` work right away. Re-running either is idempotent.\nA repeat `cotal setup` and `cotal update` refresh the copy under `~/.cotal/claude-plugin` and update\nexisting Claude plugin registrations, including older cached installations. After an npm upgrade,\nthe first regular Cotal command also refreshes these copies before accepting the new generation. Setup,\nskills installation and refresh share a machine-local lock through the final plugin check, so simultaneous\ncommands cannot overwrite a generation while another command verifies it. Disabled installations\nstay disabled during an update. Claude Code can apply an update run in a project folder to an\nenclosing project that has its own installation, so the folder's local or project installation stays\nold. That installation does not stop the refresh or later commands: Cotal prints a warning naming the\nfolder and the `claude plugin uninstall` command to run there to remove it. A user-scope installation\nthat does not update is still an error. `cotal status` checks the resolved payload's manifest version and\nfile contents. A folder-backed plugin can retain older version metadata in Claude's installation\nrecords; that metadata does not override the files Claude loads. Running sessions keep the\ncode they loaded; reload plugins or reopen Claude to load the update.\nThe install mechanics and the invariants behind them are in\n[setup internals](setup-internals.md).\n\n`cotal setup` also installs Cotal's authored Agent Skills (`SKILL.md`, the agentskills.io format) for\ncoordinating agent teams (today `team-topology`), from one canonical source, on two channels:\n\n- **Claude Code** gets a second, skills-only plugin, `cotal-skills`, from the same `cotal-mesh`\n marketplace, at **user scope** (machine-wide). The Claude connector declares and implements this\n setup provider, including the marketplace assets and native plugin commands; the base CLI only passes\n the vendor-neutral Agent Skills directory. The plugin carries no code and no core dependency,\n and uninstalls on its own with `claude plugin uninstall cotal-skills --scope user`. Its plugin version\n is stamped from the running CLI release. `cotal update` refreshes an existing skills plugin without\n enabling it or installing a removed one; `cotal setup --skills` also installs missing skills. `cotal setup` installs it on first run and on repeat\n runs, so upgraders are not left behind. The same provider reports the plugin and skills plugin rows\n in `cotal status`, which point a stale or missing skills plugin at `cotal setup --skills`.\n- **Every other harness** (Codex, Cursor, OpenCode, Gemini CLI, Windsurf/Devin) reads the cross-vendor\n `~/.agents/skills/` directory convention, which has no remote index, so `cotal setup` **reconciles** it\n (and `cotal setup --skills` does only that):\n it installs/updates each Cotal skill, backs up a copy you have edited to `SKILL.md.bak` before\n replacing it, and removes a Cotal skill that is no longer shipped. Only skills Cotal owns are touched;\n your own or third-party skills there are left alone. `cotal status` reports whether the drop is current,\n stale, missing, or has a retired skill to reconcile, and names `cotal setup --skills` as the remedy. This is the working cross-vendor path.\n\nCotal also generates an [Agent Skills discovery index](https://cotal.ai/.well-known/agent-skills/index.json)\non cotal.ai, but that RFC is still a draft with no harness consuming it yet, so it is a forward bet,\nnot a channel to rely on today.\n\n## Enable channels for your organization\n\nFor claude.ai Team and Enterprise organizations, channels are blocked by default. An organization\nOwner must enable them in [Organization settings > Claude Code > Channels](https://claude.ai/admin-settings/claude-code),\nor set this organization-managed setting:\n\n```json\n{\n \"channelsEnabled\": true\n}\n```\n\nUsers cannot override this setting through local or project settings. Cotal's\n`--dangerously-load-development-channels` launch flag cannot override it either. Enable channels\nbefore launching the sessions that need them.\n\nWhen channels are blocked, Cotal's MCP tools and hook-based inbox delivery still work when the agent\ntakes a turn. An incoming message cannot wake an idle Claude Code session. A successful spawn or a\nconnected MCP server does not prove idle wake is available; check Claude's startup notice before\nleaving agents unattended.\n\nPersonal Pro and Max accounts without an organization do not need this admin opt-in. Sessions still\nneed to opt into their channel, which Cotal's managed launch does for its own server. See\n[Anthropic's channel controls](https://code.claude.com/docs/en/channels#enterprise-controls) for\norganization policy and other authentication modes.\n\n## Spawn a session\n\n```bash\ncotal spawn # foreground: your default agent, in this terminal\ncotal spawn dave --detach # supervised: the manager runs it in a PTY\n```\n\nA spawn resolves a persona from `.cotal/agents/<name>.md` ([agent files](agent-files.md));\n`--model`, `--variant`, `--cwd`, `--prompt`, ACL overrides, and `--share-tools` apply to\nboth forms ([run a mesh](run-a-mesh.md) has the full resolution rules). The session joins\nwith identity from its environment and auto-registers presence by the time it is\ninteractive.\n\nInside the session, the agent orients with one read-only tool, `cotal_orientation`: its\nidentity, the channels it reads and may post to, its capabilities, the tools available,\nwho's present, and unread counts. The full tool surface is the\n[MCP tool catalog](mcp-tools.md). In auth mode the team-supervision tools\n(`cotal_spawn` / `cotal_persona` / `cotal_personas`) are injected **only** for personas declaring\n`capabilities: [spawn]` (the same grant that opens the privileged control subject), so an\nagent's toolset matches its declared capabilities. `cotal_run` is gated separately by\n`run`; use `capabilities: [spawn, run]` for both. Fresh setup defaults include both.\nSee [workflow tool setup](workflows.md#from-an-agent-session) for a first run and missing-tool checks.\nClearing retained history is\noperator-only ([run a mesh](run-a-mesh.md)), never an agent tool.\n\n## How it binds\n\nClaude Code exposes four integration surfaces, and three of them collapse into a single\ndual-purpose MCP server:\n\n| Surface | Mechanism |\n|---|---|\n| Outbound, ambient | command lifecycle hooks (`node dist/hook.cjs`) relayed to the connector over its authenticated local control socket (presence, activity) |\n| Outbound, deliberate | MCP tools `cotal_send` / `cotal_dm` / `cotal_anycast` (+ `cotal_feedback`) |\n| Inbound, pull | MCP tool `cotal_inbox` (same server) |\n| Inbound, push | Channel nudge + hook drain (below) |\n\nThe manager launches the *real* `claude` (no wrapper):\n\n```\nclaude --mcp-config '{\"mcpServers\":{\"cotal\":{\u2026}}}' \\\n --dangerously-load-development-channels server:cotal \\\n --permission-mode bypassPermissions\n# env: COTAL_SPACE, COTAL_NAME, COTAL_ROLE, COTAL_CHANNEL=1, plus claude's documented auth vars\n```\n\n- **Tool permissions.** A managed seat has no human at its terminal, so it launches with\n `--permission-mode bypassPermissions`: a prompt under the default mode would stop its first mesh\n tool call until someone answered it, and nobody can. To get prompts back, pass\n `--opt permission-mode=default` (or another mode); the connector adds the default only when the\n launch options carry neither `permission-mode` nor `dangerously-skip-permissions`. Claude refuses\n the bypass mode as root and under a policy that disables it; such a host must pass a mode.\n\n- **Model auth.** Locally, `claude` still reads macOS Keychain / `~/.claude`. In a container or\n CI there is no Keychain, so the connector forwards the documented credential set:\n `CLAUDE_CODE_OAUTH_TOKEN` (from `claude setup-token`), `ANTHROPIC_API_KEY` /\n `ANTHROPIC_AUTH_TOKEN`, and the cloud-provider flags plus their credential vars. Host-session\n markers (`CLAUDE_CODE_CHILD_SESSION`, `CLAUDECODE`) stay out so a nested seat still saves a\n transcript. See [Deploy](deploy.md).\n- **Persona privacy.** The persona body is written to a private file and Claude receives only\n `--append-system-prompt-file <path>`. The body never appears in the spawned process argv. The\n carrier is a 0600 file inside a 0700 directory on POSIX, with equivalent owner-only ACL hardening\n on Windows. That is OS-user isolation: any process running as your user can read it while it\n exists. The manager or the foreground `cotal spawn` removes it, and the shared-server MCP config\n file, once it has proved the `claude` process gone. If the launcher is killed first, a watcher\n started beside `claude` removes them when `claude` exits.\n- **MCP servers.** A spawned agent loads Claude's native MCP sources alongside Cotal and declared\n sharing overrides. Explicit `--share-tools` selections and carried private homes keep strict\n isolation. Claude still applies its own server approvals, disabled settings and managed policy\n (see below).\n- **Lean MCP server.** Every session runs its own cotal MCP server, so Node starts it with\n `--optimize-for-size --max-semi-space-size=1` to shrink its memory footprint. Managed launches and\n the plugin's `.mcp.json` (personal plugin sessions) both pass these flags. `--lite-mode` and\n `--jitless` would save more, but they remove `WebAssembly`, which Node's built-in `fetch` needs,\n so WebAssembly stays enabled.\n- **Light hook.** Claude Code starts the plugin's hook as a new process for every tool call, prompt\n and stop. The hook imports `@cotal-ai/core` only through small subpaths such as\n `@cotal-ai/core/peer-label`, never the package root, which would bundle YAML and NATS into it.\n `pnpm smoke:claude-hook-weight` holds that.\n- **Installed plugin.** The plugin is installed once, at user scope (`claude plugin install\n cotal@cotal-mesh --scope user`), because its hooks bind only to an *installed* plugin. Earlier\n releases installed it per project, at local scope; the next `cotal setup` removes those installs and\n keeps the user-scope one. If Claude Code cannot change a project's settings, setup still removes\n the others, then fails naming that project and the `claude plugin uninstall` command to run there. A session Cotal launches loads the package's own copy with `--plugin-dir`,\n which replaces the installed one for that session, so it still runs one cotal server.\n The repo's `.claude-plugin/marketplace.json` lists the committed plugin tree under\n `claude-plugin/`, which each release regenerates with the built bundles, the skills and the\n release version, so an install from the repo or from a pinned commit runs without a build\n ([Release](release.md)). `cotal setup` (npx, no clone) materializes the same marketplace under\n `~/.cotal/claude-plugin/` from the installed CLI (each plugin dir is rebuilt from scratch and\n atomically replaced, never merged, so no stale file rides in). The\n `cotal-skills` plugin installs from that same marketplace at user scope (`claude plugin install\n cotal-skills@cotal-mesh --scope user`); its manifest and install behavior ship inside the Claude connector, and\n its version tracks the CLI release so updates land.\n- **Identity-gated.** A launched session joins under the identity in `COTAL_NAME`, `COTAL_LINK`\n or `COTAL_AGENT_FILE`. A plain `claude` with none of them joins only when its conversation calls\n `cotal_connect`, so your own sessions do not appear as stray peers. Until then its MCP\n server builds no mesh agent and opens no broker connection. See\n [Sessions Cotal did not launch](#sessions-cotal-did-not-launch).\n- **Hands-free.** The dev-channels flag prints a one-time confirm prompt. The runtime waits for the\n dialog title in normalized terminal text and presses Enter once when it appears, so startup speed\n does not affect a supervised launch. Claude skips that prompt when channels are off for the\n session, for example under a Team or Enterprise organization whose managed settings do not set\n `channelsEnabled: true`, and opens its input screen instead. The runtime then ends its watch\n without a key once the input screen's footer shows `(shift+tab to cycle)` or `? for shortcuts` on\n two screen reads in a row. That seat gets mesh messages through its hooks only, so a message does\n not wake it while it is idle. The PTY runtimes read the child's screen, and the tmux, cmux, Orca\n and Herdr runtimes read the pane's screen. If neither the prompt nor the input screen appears\n within 15 seconds, the seat ends with a bounded error naming the unmatched prompt instead of\n hanging silently or answering another dialog. The PTY runtime writes that error to the seat's output and\n stops the child as a graceful stop does, so a child that ignores SIGTERM is killed 3 seconds\n later. Confirmation ends before the stop begins, so later child output receives no startup\n response. The other runtimes write the error to the manager's log.\n- **Trusted directory.** An authorized supervised spawn accepts the selected workspace through\n Claude's native trust dialog, which Claude draws in one of two layouts. When a numbered list\n shows Yes first and selected, the runtime confirms it with Enter. When No comes first and is\n selected, the runtime moves to Yes and confirms only after seeing Yes selected. It never confirms\n while No is selected, sends each response once, and stops watching after the\n development-channel confirmation or on the input screen. A changed or unsupported dialog receives no guessed input\n and still fails within the startup bound. Claude saves trust with its own repository and linked\n worktree rules; Cotal does not rewrite the manager's configuration. Accepting trust enables the\n workspace's hooks and settings, which can select a tool permission mode when no launch flag\n overrides it. Cotal adds no tool-permission bypass flag. Review the checkout before authorizing\n a spawn there. A foreground `cotal spawn` keeps the trust dialog in your terminal.\n\nInbound mesh messages arrive in context as\n`<channel source=\"cotal\" from=\"bob\" kind=\"dm\" \u2026>\u2026</channel>`: each meta key a tag\nattribute the agent can read for routing.\n\nThe consuming connector holds a crash-safe claim for its broker target, space, principal and\nlifecycle on this OS user account. A second patched session on the same host refuses before\npulling that inbox, even when its launch files and control socket differ. Reconnect keeps the\nclaim. Hooks and non-consuming helper connections do not claim it. The control listener binds\nbefore the broker connection and never removes an existing socket path.\n\nThis is cooperative same-host containment. Other hosts, broker URL aliases and older clients\nare not fenced. Preserve the existing session before choosing a distinct actor for a new\nlaunch. An uncertain shutdown keeps custody until the consuming process exits.\n\n## Sessions Cotal did not launch\n\nA Claude Code session you start yourself, where the `cotal` plugin is installed, gets the tools the\nhosted connector offers: `cotal_connect`, `cotal_wait`, `cotal_disconnect` and the routed mesh tools.\nNothing joins until the conversation calls `cotal_connect` with a name. The plugin's server then runs\n`cotal app-identity provision` with the CLI that `cotal setup` recorded beside the plugin\n(`dist/cli.json`), so it does not need `cotal` on `PATH`. Each conversation gets an identity of its\nown once a person approves it. To choose a registered mesh for this conversation, pass `space`:\n\n```json\n{\"name\":\"my-session\",\"space\":\"cotal_cloud\"}\n```\n\nThis is the native plugin's `cotal_connect` argument. It takes precedence over the saved selection\nand the project folder, without changing either. `cotal space` lists this machine's registered\nspaces. A blank or unknown space is refused; it is never replaced by another mesh. With no `space`,\nthe plugin uses the mesh a bare `cotal spawn` from the session's directory would pick, following\n[the CLI's selection rules](cli.md#selecting-a-space). Do not rename `.cotal` directories or add\n`COTAL_SPACE` to choose a personal conversation's target.\n\nA connected conversation must call `cotal_disconnect` before choosing another name, space or grant.\nBefore it connects, you can change a pending name or permissions by calling `cotal_connect` with the\nnew values and no `approval_id`. You do not need to approve the unwanted request or start another\nconversation. The old watcher stops, the saved request is recovered and retired, and the replacement\ngets a fresh approval. Changing the space follows the same cleanup rule. If approval raced the\nchange, its issued identity is retired before the replacement is requested; its grant is never\nreused. An unconfirmed retirement blocks the switch and keeps the old recovery record. A second\nhost cannot replace a request while its original consumer is still alive.\n\nThe hosted HTTPS connector chooses its space during browser authorization. Its OAuth grant is\nbound to that space; a native `space` argument cannot retarget a hosted grant.\n\nA static-auth or open mesh has no page where a person approves a connection, so the plugin\nopens no identity there. A plugin that `cotal setup` did not install has no record, and\n`cotal_connect` names that remedy.\n\nPersonal native Code sessions can join a signed-in mesh whose authority offers connection approval.\nThe plugin binds the real native session and transcript before requesting an identity. Each\nconversation gets a separate actor. A new identity is issued only after a person approves it in the\nauthority's signed-in web page. A model's tool arguments, a client flag or a native tool approval\nnever approve it. The request first comes back pending with the page's address and its expiry, and\nnothing is issued yet. Connecting again sends the same request and secret: while it is pending it\nanswers the same page, and once approved it issues the identity. A denial or an expiry drops the\nrequest, and the next connection asks again. No login proof reaches the conversation or its tools.\n\nWhile a bound Code conversation's request is pending, the plugin's server replays that same request\nto the authority every ten seconds, serialized with the conversation's own `cotal_connect`, until\nthe authority records a decision, the request passes its expiry, or the session ends. The server\nissues nothing by itself: an approved replay receives the identity the authority issued for that\nrequest, and the server holds it, unclaimed and unconnected, for the conversation's own\n`cotal_connect` with the same name and the `approval_id` from its pending result. The decision\nreaches only the native session that asked, through its wake reader, as one fixed line: approved\n(naming the `approval_id` to finish with), denied, or expired. Nothing else is written that way, and\nthe native module admits no other shape. A session that ends before claiming an approved identity\nhas it retired; a request still pending when the session ends stays saved, and the CLI\nfinishes it before that conversation's next request. An `approval_id` that is not this\nconversation's own pending request is refused before any request is sent. Desktop chat has no\nnative reader, so there the person asks Claude to retry.\nA remote mesh must advertise conversation identity v1 and connection approval v1 on its pinned HTTPS\ndiscovery endpoint. An absent capability, a different origin or a changed registration is refused\nbefore authenticated issuance, for a request without a selection too. There is no immediate-grant\nfallback. A signed-in mesh whose authority this machine holds has no approval page, so it issues no\nnew identity either.\n\nEvery identity sends and receives direct messages and publishes its own activity on its own event\nchannel. These come with the identity and are not optional, so the approval page shows them before\nthe person decides. Native activity sharing includes the messages the person types, assistant\nreplies, and each tool call's name with its start and end, after activation. It is not only presence\nor status. Tool arguments and tool results are withheld from the event channel, as on every connector\n([Event plane](#event-plane)). While a tool waits for permission, presence names it with a short\ndetail such as its command, and every roster reader sees that. Reasoning is not shared by default,\nand earlier or pre-approval history is not shared. The space's activity read permissions determine\nwho can read the event channel. Decline the connection if you do not want this sharing.\nA request without a selection proposes nothing beyond these rights: no role,\ncapabilities or ordinary channel reads.\n\n`cotal_connect` in a native Code session also takes a selection: `channels` to read\n(`access: \"read\"`) or read and post to (`\"read-post\"`), a `role`, and `capabilities` (`spawn`,\n`run`). The selection is a proposal. The person may approve it, change it within their own current\naccess, or approve nothing beyond the identity's own permissions. The issued grant must match what\nthe person approved item for item, or the connection is refused. When the authority refuses an item\noutside your own grant before asking anyone, nothing is issued and the reply names every refused\nitem. There is no fallback to the event-only identity. Its own event channel stays the first post\nentry. A remote mesh must also advertise `conversationIdentitySelection` version 1, or a selection\nis refused before any request.\nEndpoint calls are not selectable through this connection approval contract, which accepts only\nan empty endpoint selection. These conversations do not offer `cotal_describe` or `cotal_invoke`.\n\nA connection is offered the tools its grant can use: `cotal_spawn`, `cotal_persona`,\n`cotal_personas` and `cotal_despawn` with `spawn`, `cotal_run` with `run`, and `cotal_reconnect`.\nIts manager calls exchange the conversation's own secret for a manager-call view, so they carry only\nits grant and name the manager instance when more than one could answer. A conversation is not a\nmanaged seat. `cotal_despawn` without a name refuses rather than stopping your app, and\n`cotal_disconnect` retires only that connection. No run turn is addressed to it, because a workflow\naddresses turns to agents it spawned, so `cotal_yield` is not offered. Persona text and model pins\ndo not change the conversation's own instructions or model.\n\nThe plugin's ordinary command hooks supply the session and transcript binding. Its native module\nuses that binding without intercepting `classic.*` events, which Claude's built-in security guard\nwithholds from user plugins. The guard stays enabled. If an organization disables command hooks or\nnative modules, its administrator must permit them before this connection path can work. Command\nhooks still report lifecycle status and activity; the native module owns incoming message delivery.\n\nA signed-in identity keeps its request and secret in private local recovery state. A lost create\nresponse retries the same request without rotating its secret or allocating another identity.\nRetiring a request that is still pending drops it, and no identity is issued for it. An identity\nissued before connection approval keeps working, and its recovery stays exact. Another plugin server reuses that identity only after the process that held it no longer exists.\n`cotal_disconnect`, session end and a signal request retirement of that lifecycle only. Other\nconversations stay connected. When the mesh reports retirement pending or does not confirm it, the\nconversation stays disconnected and keeps its saved record. Every tool then reports the same\nunfinished cleanup with the mesh's retirement phase. Calling `cotal_disconnect` again retries it at\nonce. `cotal_connect` retries it too, and connects with a new identity once the mesh confirms\nretirement. A restarted plugin server recovers the same way. After each connection, the plugin\nretires the same mesh's saved identities whose plugin server process no longer exists, such as one\nkilled before it could retire them. This runs in the background, so the connection does not wait\nfor it. A server process that still exists keeps its identity, and a retirement that does not finish\nstays saved for a later run. A mesh requiring events refuses a connection without a native\ntranscript source.\n\nThe gateway's MCP annotations distinguish read-only queries from updates. Orientation, connection\nstatus, the roster, the docs and channel information are read-only. Sending messages, connecting and joining\nchannels are additive. Inbox reads and waits consume messages; status, channel mode, leaving and\ndisconnecting can replace or remove state. These hints do not grant access or override Claude's\npermission settings or an organization's approval policy.\n\nA message for a connected conversation starts a turn through the plugin's\n[native hooks module](https://code.claude.com/docs/en/plugins/mods/overview). This works in supported\nClaude Code hosts, including local sessions in the Desktop app's Code tab. The module runs a local\nwake reader as a child of the same host that runs the MCP server. Each server uses its own socket\nin a private directory. Short messages arrive in full with their sender, message ID and reply\ncontext. Claude can answer these without calling `cotal_inbox`. The module uses Claude's\nplugin-sender attribution, never the person's own identity. While Claude is busy, delivery happens\nat a main-conversation tool boundary before the next request. A request already being sent has a\nfixed message snapshot. A later arrival waits for another tool boundary or starts its own attributed\nturn when the current turn ends; storing a note alone does not count as delivery to that request.\n\nEach inline batch carries at most eight whole messages. Each rendered message must fit within\n1,024 Unicode code points, and the encoded socket frame must fit within 4,096 characters. Larger\nmessages remain queued. A notification-only reader or an oversized message uses\n`Cotal: a new message for connection_id <id>. Read it with cotal_inbox.` and a bounded preview.\nA preview is incomplete; Claude reads its contents through the inbox. An inline batch also names\nhow many other messages remain pending.\n\nThe transcript shows a compact Cotal indicator. Expand it to inspect the delivery or preview.\nThis changes only the display. Presentation observes only prompt rows; in-turn appends use their\nreturned row ID, without observing protected notes or skill output.\n\nSocket writes and previews do not acknowledge messages. A complete inline batch is committed only\nafter the native host confirms admission to the bound conversation. An idle prompt also needs its\nstored prompt row; a busy append needs its returned row ID and complete stored text. An offer waits\nfor a busy conversation's next safe boundary while its reader remains connected. Admission after\nclaim has its own deadline. This confirms host handoff, not that\nClaude understood or completed the work. Delivery remains at least once after an unknown result.\nMessage IDs distinguish retries from new requests.\n\nA wake follows the rule `cotal_wait` follows and does not compete with an active wait. Confirmed\ninline delivery rearms the next wake without an inbox call. A replacement reader reoffers unread\nwork. An offer from a closed connection cannot suppress its replacement connection's messages.\nClaude's Stop and organization controls still apply. Refused delivery stays pending without\na retry loop; genuine person-origin input resumes it with a fresh offer. `cotal_connect` reports\nwhether the incoming reader is attached; attachment alone does not prove native admission.\nWhen native modules are unavailable or blocked, the conversation can listen with `cotal_wait`.\n\nClaude Desktop chat does not run a plugin's local server. On macOS and Windows, where Claude Desktop\nis installed, `cotal setup` adds a `cotal` server to Desktop's own config,\n`claude_desktop_config.json` (Settings > Developer > Edit Config), and keeps every other entry there.\nIt runs the same server with `--desktop`, by the absolute paths of the node that runs the CLI and of\nthe plugin copy under `~/.cotal/claude-plugin`. Quit and reopen Claude Desktop to load it. A repeat\n`cotal setup` and `cotal update` keep the entry current, `cotal status` reports it, and a config\nfile that is not valid JSON is refused and left as it was. Desktop reads that file when it starts\nand writes the copy it holds back while you use it, so an entry added while it runs is lost. On\nmacOS, setup and update therefore leave the file alone while Claude Desktop is running, finish the\nrest of their work, and say so. Quit Claude Desktop, then run `cotal setup` again from a terminal\noutside it; a terminal in its Code tab keeps Desktop running. On Windows they still write while\nDesktop runs, because the loss was not reproduced there. Setup can use Desktop's bundled Code\nexecutable when there is no standalone `claude` CLI. Its installed MCP and command hooks use the\nrecorded Node executable, so the app need not find Node on `PATH`. On Linux, setup writes nothing\nfor Desktop Chat.\n\nDesktop Chat supplies no native Code transcript binding, so this local server cannot join a\nsigned-in space, such as one on Cotal Cloud, and refuses before identity issuance. Desktop Chat joins\na signed-in space only through the hosted connector that the space's discovery document advertises\nas `cloudCapabilities.hostedConnector`, version 1 with an HTTPS URL. Add that URL in Claude under\nSettings > Connectors > Add custom connector. Setup for a signed-in space writes no Desktop entry and\nprints the URL instead. `cotal status` prints it on the Claude Desktop row when the selected space is\nsigned in, and the refusal names it. When the space advertises none, all three say so. The Desktop\nserver's instructions tell the model to use the hosted connector for a signed-in space. The hosted\nconnector is a separate connection path; it does not supply autonomous chat wake.\n\nDesktop's working directory is not your project, so its mesh is the `cotal use` mesh, else the only\none running. Desktop runs no monitor, so a Desktop conversation hears messages when it calls a tool,\nand keeps calling `cotal_wait` when you ask it to keep listening. A chat with no turn running cannot\nbe woken by any server: Claude's chat surfaces start a turn only from the person.\n\n## How messages reach the session\n\nDurable deliveries land in the connector's inbox from JetStream consumers\n([SPEC \xA78](../SPEC.md#8-nats--jetstream-binding)); live channel traffic can instead arrive\nthrough an at-most-once core subscription. A durable message sent while the agent is busy\nor offline waits on the stream. Two things move a message from inbox to model; one\ndelivers, the other only wakes:\n\n- **Hook drain (delivery).** `SessionStart` / `UserPromptSubmit` hooks read automatic inbox items and\n inject them as `additionalContext`. This is the single authoritative path: deterministic and works\n on any Claude Code build. Quiet ambient is excluded and stays buffered for `cotal_inbox`.\n A message is **acked only once the hook reply carrying it has cleared both legs of its journey**:\n the connector's control socket to the hook process (which gives up after 2s), and the hook\n process's own stdout to Claude Code (which it force-exits 1s after starting to write). The relay\n sends a receipt back down the control socket from that stdout write's callback, and only on a\n clean write (a runtime whose pipe has gone away fails it), and the connector treats that receipt,\n not its own socket write, as delivery. So a large injection killed mid-flush, or one written to a\n broken pipe, leaves the message un-acked and JetStream redelivers it. What this does *not* prove is\n that Claude Code read or applied the reply: a payload small enough to fit the pipe buffer is\n reported written the moment the kernel takes it. That residual is why the path errs toward\n at-least-once rather than treating a confirmed write as a confirmed read. Acking when\n the reply was merely *formatted* meant a lost reply was a lost message: it was already marked\n handled, so its own redelivery was silently acked on arrival.\n A hook whose handler throws still returns an empty reply so the session is never blocked, and\n that reply carries nothing, so it commits nothing: the batch it had started to surface stays\n un-acked and goes out on a later frame. A manager-launched seat pulls its run turns\n (`turn-pending`) when its manager's turn notice arrives, when it connects, and after a reconnect\n while it holds a turn. The manager repeats a notice until the seat pulls the new turn, so an idle\n seat sends nothing. A pull that fails because the manager is down or unanswered is\n retried once every 15 seconds until one succeeds; a refusal is logged and not retried. The seat\n also drops any `turn-pending` row that breaks\n the manager contract, such as one with no integer deadline, and says so once in its log. A reply\n with no `turns` array changes nothing: the seat keeps the turns it already holds.\n This errs toward **at-least-once**: if a reply lands but its confirmation does not, the batch is\n surfaced again and flagged as a possible repeat. A duplicate injection is noise; a buried DM stops\n the peer answering at all.\n- **Channel nudge (wake).** An arriving message fires a `notifications/claude/channel`\n event that wakes an *idle* session into a turn, so the drain runs *now* instead of at\n the next prompt. The nudge never acks anything. A nudge that the host rejects is retried with a\n bounded backoff while anything is still pending. For an idle session it is the only wake source,\n so dropping it means silence until someone types. When the channel becomes active, the connector\n first re-fires a focus mention remembered during startup, otherwise one buffered wake. A rejected\n push keeps its bounded retry, and JetStream redelivery remains the durable backstop for unacked\n inbox items. Neither a redelivery nor that retry repeats a nudge already pushed for that message,\n whether the message had its own nudge or was counted in a batch one, so a session held in a long\n tool call gets one nudge per message. Once a hook frame carries the message, or the push that\n announced it fails, its next redelivery nudges again, so a reply that never reached Claude Code\n still recovers. If the channel cannot run at all, delivery still waits for the next hook. Live-only\n traffic has no durable retry.\n\n**Two priority tiers.** A *directed* message (DM, anycast, or a channel message that\n`@mentions` us) always nudges. *Ambient* channel chatter does not nudge mid-turn; it\naccumulates, and the `Stop` \u2192 idle transition fires one batch nudge so the backlog drains\ntogether.\n\n**Constraints (accepted).** Channels are a Claude Code research preview (\u2265 v2.1.80;\npermission relay \u2265 v2.1.81): Anthropic auth only, admin-enabled on Team/Enterprise, and a\ncustom channel needs the `--dangerously-load-development-channels` launch flag. The hook\ndrain does not depend on any of that; the channel only adds \"wake me when idle.\"\n\nThe same channel also relays **tool-permission requests** onto the mesh, so a peer (a\nhuman at the CLI, a policy node) can approve or deny an agent's pending tool call through\nCotal rather than a per-terminal prompt.\n\n### Attention\n\nAn agent picks how aggressively peer traffic reaches it with\n`cotal_status({ attention })` (three modes, orthogonal to presence):\n\n| arrival | open (default) | dnd | focus |\n|---|---|---|---|\n| directed (dm / anycast) | wake + inject | wake + inject | wake + inject |\n| channel `@mention` | wake + inject | wake + inject | ack-drop; wake to *pull*; not injected |\n| ambient channel chatter | wake when idle; hold while working | never wakes; injects next turn | ack-drop; recall via `cotal_inbox` |\n\nPer-channel overrides refine this: **quiet** (delivered, never wakes; `@mention` still\nwakes) and **muted** (dropped on receive, mentions included; DMs/anycast unaffected), set\nwith `cotal_channel_mode` or as agent-file defaults (`quiet:` / `muted:`,\n[agent files](agent-files.md)). A per-channel override is the final word for that channel.\nQuiet ambient is pull-only: it never hitchhikes on a human prompt, DM, mention, or other\nconnector-driven turn. `cotal_inbox` explicitly surfaces and clears it. A quiet-channel\n`@mention` remains automatic and injects normally.\n\nA pull is bounded too, and clears only what it hands over. One `cotal_inbox` call carries at most a\nreceivable window (direct messages and role requests first, then channel traffic, replayed history\nlast); whatever does not fit stays buffered, is named in the reply, and comes back on the next call.\nA message too large for one whole response is delivered in parts: once no smaller mail is waiting,\neach call carries its next part, and it is cleared only after its last part goes out, because clearing\nwhat was not handed over is the loss this bound exists to stop.\nThat matters most on the path where it is easiest to lose mail: reconnecting brings a channel-history\nreplay with it, so the largest payload and the least expendable message arrive in the same read.\n\nThe local inbox is bounded. On pathological overflow it evicts pull-only items first, then other\nchannel traffic, and a direct message or role request only when the whole buffer is directed mail.\nIt never evicts a message the connector is handing to the session or a turn is still carrying.\nAn evicted channel item is acknowledged. An evicted direct message or role request never is, and\nthe broker redelivers it after the ack wait until a redelivery finds room. A direct message stays\npending on the session's DM durable, where `cotal deliver pending <name>` counts it. A role request\nstays on its role's shared queue, which that command does not read. A full inbox therefore delays\ndirected mail until the session drains it.\nIf the bounded live/durable classification guard also fills, the connector fails closed:\notherwise-normal ambient becomes pull-only until restart. Muted hard-drop and normal focus recall\nstill take precedence. Focus also keeps a bounded exclusion list so mode toggles cannot recall\nquiet/muted traffic; if that safety bound fills, recall skips the affected channel and reports it\nas incomplete rather than risk resurfacing excluded content. Recall cannot tell one message with an\nempty id from an identical one with another disposition, so in focus such a message is held in the\nlocal inbox as pull-only instead of being dropped, and a mention of it still wakes the agent. When\nthe session settles an id-less copy, it reads the chat stream's last sequence. Identical copies\narrive in stream order, and those reads can answer out of order, so the first read that can see the\ncopies binds them in arrival order, latest first, each to the latest unbound stream copy at or below\nthe lowest sequence read for it or any later identical copy.\nWhile the connection stays up, every stream copy at or below that sequence reached the session\nfirst, so a later identical copy sent during a reconnect gap is above it and stays unbound. A copy\nthat arrives while a read runs may not be in that read, so it binds nothing there and recall reads\nthe channel again, up to three times; if it still could be a stream copy in the last read, recall\nleaves that stream copy in the stream and reports the channel as incomplete. A settled copy a\ncomplete read cannot bind is behind the focus start or out of retention, and is forgotten. Recall\nhands back into the inbox only the stream copies nothing is bound to, such as one sent during a\nreconnect gap or one the inbox evicted on overflow, and `cotal_inbox` hands each over once. Overflow\nfrees only the evicted copy, held or quiet, and a copy no read has bound yet keeps its place in\narrival order, so an identical muted copy stays out of recall. When\nthe inbox is full, recall leaves them in the stream for a later call and reports the channel as\nincomplete. A history read that fails, or a channel with replay off, settles nothing and is reported\nas incomplete, and recall calls run one at a time. If the sequence read for a settled copy fails,\nor answers only after the connection dropped, recall skips that channel for the rest of the focus\nperiod and reports it as incomplete. Recall cannot tell a late copy of a message it handed back from\na new identical message, so every copy takes its own disposition: a new identical quiet mention is\nstill delivered automatically, and a late copy can surface a second time.\nA recalled message that already went out in part is read to its last part, even if an exclusion\nlands after its first part. One session reads its inbox one call at a time: a `cotal_inbox` call\nthat overlaps another waits for it to finish, so neither decides from a view the other has already\nmoved past.\nIf the separate hard-drop disposition guard fills, channel traffic is dropped for the rest of the\nsession rather than risk a late copy bypassing an earlier muted/focus decision; DMs and anycast are\nunaffected.\n\nAttention is **advisory UX, not a boundary**: any peer can wake a dnd/focus agent by\nnaming it, and `muted` means \"I opted out of receiving\", not \"the channel is blocked\";\nthe broker still authorizes and delivers. Focus's real effect is shrinking the\nuntrusted-ambient injection surface (only subject-authenticated dm/anycast auto-inject).\nIt resets to **open** on `SessionStart`, so a restarted agent never stays silently deaf.\nYour attention is mirrored into presence so peers can see it.\n\nWhatever does reach a turn is framed so a peer cannot write the frame. A line that begins at column\nzero is written by the connector; one message is one line plus indented continuations, with the\nsender inside a single bracket pair. A message body, a sender name and role, and a service or\nchannel label are all peer-controlled, so each passes through the same neutralization the\n`cotal_inbox` reply uses: no line break a splitter may honour and no bracket survives into a\nrendered attribution. This matters more for an injected block than for a reply, because the agent\ndid not ask for it and so never had the chance to distrust it.\n\n## Presence mapping\n\nThe connector wires a small subset of Claude Code hooks to presence states; presence is\ncoarse, and \"what it is doing\" rides on activity updates. Presence is **advisory**: a presence\npublish that fails (the endpoint mid-reconnect, say) is swallowed and never prevents the same hook\nfrom delivering messages or flushing held ones.\nA `SessionStart` during an open turn, including compaction, preserves the current `working` or\n`waiting` status until `Stop`, `StopFailure`, or `SessionEnd` closes the turn.\n\n| Hook | \u2192 state |\n|---|---|\n| `SessionStart` | `idle` only when no turn is open (join; surfaces the inbox; captures the live model into `meta.model` when no pin) |\n| `UserPromptSubmit` | `working` (turn starts; surfaces the inbox) |\n| `PreToolUse` | no change; records *what* is about to run, so a permission wait can name it |\n| `PostToolBatch` | `working` when the main agent's batch resolves after a prompt left the seat `waiting`, which clears its condition so the turn's `Stop` yields; a subagent's batch (it carries `agent_id`) or one relayed after the turn's terminal changes nothing |\n| `Notification` (`permission_prompt` / `agent_needs_input`) | `waiting` with condition `approval` / `input` (activity leads with the pending tool, e.g. `Bash: git push \u2026`) |\n| `Stop` / `StopFailure` | `idle` (turn done / died on an API error; flushes anything held while busy). `StopFailure` also relays Claude Code's native error value as `condition.source` and maps it to the closed condition vocabulary. On the [event plane](#event-plane) it closes the run with `RUN_ERROR`. |\n| `SessionEnd` | `offline` (graceful leave) |\n\nThe connector also leaves gracefully when its stdin closes. An MCP client closes it to end the\nsession, and a killed `claude` closes it with no `SessionEnd`, so a dead session drops off the\nroster instead of staying on it as a live peer.\n\n`StopFailure` maps `rate_limit` and `overloaded` directly; auth and credential failures to\n`auth`; account and billing failures to `billing`; `invalid_request` to `request`;\n`model_not_found` to `model`; `server_error` to `server`; `max_output_tokens` to `context`; and\n`unknown` to `failed`. The native value remains in `condition.source`.\n\nHooks are relayed over the connector's **authenticated** local control endpoint (per-user\nsocket + per-launch token, constant-time checked), so a local process that finds the path\nstill can't drive presence or stop the agent. Claude Code's own\n[hooks reference](https://code.claude.com/docs/en/hooks) lists every hook event. The hooks the\nplugin registers, with their matchers, are listed in the\n[adapter README](../extensions/connector-claude-code/README.md#claude-code-hooks).\n\n## Event plane\n\nA connected native session publishes a structured account of its activity: the messages a person\ntyped, assistant replies, run boundaries, and each tool call's start and end. Tool arguments and\nresults are withheld (below), and a turn a peer message started opens a run without republishing\nthat message. Reasoning is off by default. Programs can read these events directly. The launcher\nsets `COTAL_EVENTS` by default; pass `--no-events` to opt out on an unrestricted\nspace. A user-auth registration with `policy: { events: \"required\" }` carries `eventsRequired` in the\nprivate launch material, so the connector arms even without `COTAL_EVENTS`; `--no-events` is refused.\nA hand-driven user-mode session may carry the same decision as `COTAL_EVENTS_REQUIRED=1`. Its own\npublish grant must cover `events.<owner>.<actor>` or the connector refuses before joining. A personal\nnative Code connection to a signed-in mesh uses the same recorder and captures its transcript\nboundary after approved issuance, before joining. Earlier history and time spent waiting for\napproval are not published.\nIts native lifecycle drives presence and recording for that session only; a subagent or another\nconversation cannot end its main run. The event channel keeps the space's normal read permissions.\nA personal connection makes manager calls only with its own selected grant.\n\nIf the event plane stops, the seat keeps running and its mesh connection, on every connector and\non every space. The connector log records `AG-UI emitter stopped` with the reason and the delay\nbefore it re-establishes the plane: at once after a failure, then every second while the plane\nstays down, a fixed wait that neither grows nor is capped, so the plane is back within a second\nof the fault clearing. The re-establish\nis a fresh start: it reopens the write-ahead log, settles any frame it left pending, builds the\nmapper from the run the log has open, and reads on from the stored cursor, which is the same\nrecovery a connector restart runs. A failed publish or a failed log write is never retried on the\nemitter it failed on. A turn that ends while the plane is down keeps its terminal, and so does a\nturn whose terminal's own log write failed: once the re-establish has read on, the run the log had\nopen is closed with that terminal, a failure as a failure, rather than left open for the next\nprompt to close as a finish. That holds when the next prompt was already written before the plane\ncame back: the failure is published after the failed turn's own records and before the next run\nopens, in a frame of the failed run, since a run error names no run and the frame is its only\nattribution. Every turn that ends while the plane is down keeps its own terminal. A terminal belongs\nto the last turn the transcript had begun when its hook fired: the connector reports a turn's\nterminal once that turn's records are written and before the next turn's are, so the position the\ntranscript has reached at the hook names the turn, including a turn the connector opens no run for,\nsuch as a task notification, which takes its terminal and publishes nothing. A terminal repeated\nfor a turn that has one replaces nothing and is kept for no later turn. A line the connector cannot\nread, written past that position while the plane was down, does not hold the terminal: the\nre-establish reports the gap and places the terminal after the records before it. A record no read\nanswers, such as an OpenCode part still filling when its turn died, holds it no more: the terminal\nis placed after the records before it, and the turn end reads the session once rather than without end. The OpenCode source reads every record at or before the end it reported at a turn end as settled, so a part left filling by a failed turn does not stop the turns after it from publishing. A terminal reported while the first start is still waiting on the broker reaches the emitter before its first read, so a prompt written in that window does not close the failed turn as a finish. The same placement runs with\nthe plane up: a terminal reported while the flush before it is still reading, with the next prompt\nwritten before that read, closes its own turn in the place of the finish that prompt synthesizes. The seat used to exit on a space that requires events; for Claude Code the\nconnector is the MCP server, so that exit showed as the server disconnecting, and Claude Code\nrestarted it on the next tool call.\n\nA transcript line that is not a record does not stop the plane. Claude writes the transcript, and a\nwrite cut short by a full disk leaves part of one record glued to the next on a single line, which\nnever becomes readable. The connector steps over that line, keeps the records after it, and writes\n`skipped an unreadable transcript record at offset <offset> (<bytes> bytes)` with the parser's\nreason to its stderr.\n\nA new session includes its first run even when Claude writes a positional startup prompt before the\nconnector receives `SessionStart`. That from-zero read is keyed only to Claude's explicit\n`source: \"startup\"`; resumed, forked, cleared, and compacted sessions adopt at the transcript boundary\ncaptured at that adopt, before the mesh link connects, so nothing Claude appends while the connector\nis still starting up lands behind the cursor and is silently dropped. Crash recovery follows the\ncursor already stored in the event write-ahead log, regardless of the new process's startup label.\n\nClaude starts each hook in its own process, so a prompt or stop relay can reach Cotal before the\n`SessionStart` relay. The connector holds those event flushes and the terminal until `SessionStart`\nsupplies the source, then enqueues adopt, flush, and close in that order.\n\n`SessionStart` can also run before the connector process has bound its local control socket. The hook\nthe `SessionStart` relay retries only transient pre-connect listener errors, with capped backoff\ninside its existing two-second budget. Later hooks and permanent local faults still fail open\nimmediately. Once a socket has connected, a broken exchange is not retried: the connector may\nalready have handled the frame, so replaying it could apply one lifecycle event twice.\nThat retained `SessionStart` can itself arrive before Claude creates the transcript path. A genuinely\nnew startup waits up to five seconds for that file with capped backoff, and the same deadline bounds\none stalled file read; expiry fails loud instead of silently losing the first run. A forked session\ngets the same wait, because Claude copies the parent transcript into the fork's own file after the\nhook, and then adopts at the end of that copy. Resumed, cleared and compacted starts and recovered\ncursors still require their existing source at once.\n\nTool arguments (`TOOL_CALL_ARGS`) and tool results (`TOOL_CALL_RESULT`) are not republished\nonto this channel. The durable emitter drops those events before they are written to the\nwrite-ahead log, because this channel's read ACL is not the ACL the tool ran under. Content is\nmandatory on both kinds, so the event is suppressed rather than emptied or replaced with a\nplaceholder. Tool start and end still go out. A restart that finds a pending pre-fix frame\nstill carrying those kinds HALTS rather than republishing it.\n\nThe channel is **`events.<owner>.<actor>`**, named after the session's principal. What the actor\nhalf is depends on the mesh, and the difference matters when you go looking for it: on a static mesh\nit is a key the manager allocated, never the display name, so two live agents sharing a display name\ndo not share a stream; on a user-auth mesh it is the agent's own name, because that is what the\nledger row is keyed on. Spelled out again with both halves below. The launch grants publish rights\non that channel alone. A spawn\nthat asks for a *different* agent's event channel is refused at the door rather than granted, since\nthat channel is that session's event stream. The same rule runs on restart: a manager\nresume document that names another agent's event channel is refused rather than adopted, because the\nmanaged row is re-armed from that document and the credential is re-minted from the row.\n\nThe rule reads a **concrete** channel, two principal tokens and nothing else. A pattern such as\n`events.<owner>.>` is not an event channel to it and passes untouched, governed by ordinary ACL\nauthority: on a user mesh the delegation envelope, on a static mesh the spawning credential itself.\nThat is deliberate, because the pattern is the form an operator writes on purpose for an observer,\nand it is worth knowing rather than assuming the fence is total.\n\nTo let something else read a plane, grant it out of band. The refusal prints the command for the\nmesh it is running on, spelled out in full, and only that one.\n\nOn a **user-auth** mesh:\n\n```bash\ncotal actor grant <reader> --owner <owner> --scope '' --allow-subscribe 'events.<owner>.<actor>' --allow-publish ''\n```\n\nEvery field, deliberately. `actor grant` is an upsert of the whole row, so it refuses a grant that\nleaves off any of the three ACL flags. Only `--full` turns an omitted flag into the wide default\n(`>` read, `>` post, `spawn,role:default` scope), which is the opposite of what a scoped watcher is for.\n\nOn a **static** mesh there is no actor ledger for `actor grant` to write to, and the refusal says\nso; mint the reader instead:\n\n```bash\ncotal auth mint watcher --profile agent --allow-subscribe 'events.<owner>.<actor>' --provision\n```\n\nThe **agent** profile, not the observer one. `mint` reads `--allow-subscribe` only for that\nprofile, and refuses it anywhere else: `--profile observer --allow-subscribe <channel>` exits\nnon-zero and writes no creds file, because the observer profile carries a fixed read set over the\nwhole chat plane, which is the opposite of what a scoped watcher is for. The agent profile also prints the lifecycle uid the\nreader needs, since an authed consuming endpoint refuses to start without one.\n\nOn an **open** mesh there is nothing to grant: the mesh has no credentials and no ACLs, so any peer\nthat lists the channel reads it, and the refusal says so instead of naming a command. The\nown-channel rule still applies there, because a spawn is not the place to hand out a read on\nanother agent's tool inputs and outputs.\n\nTwo things a reader has to do that are not obvious, both on `CotalEndpoint`. It must pass the event\nchannel in `channels`: an endpoint reads the channels it lists, so one constructed without\nthe event channel joins nothing and the frames never arrive. And it reads history with `readHistory(channel)`, the delivery daemon's mediated read, not\n`channelHistory(channel)`: a scoped credential is denied the ad-hoc consumer the direct read\ncreates, by design. `cotal console` and the web console already do both.\n\nThe `<owner>.<actor>` pair is the session's principal. On a user-auth mesh the actor half is the\nagent's own name, so the channel is `events.<your-owner>.<agent-name>`. On a\nstatic mesh the owner half is the literal `local` and the actor is a key the manager allocated, so\nthe channel is `events.local.<key>`; the spawn reply carries that key as `id`. Note\nthat `cotal console` and the web console keep event channels out of their channel lists on purpose,\nsince a plane is a machine feed rather than a conversation; they draw the frames when you open the\nchannel by name.\n\nThe rule governs the manager's doors, which are the ones a caller other than you can reach. A\nforeground `cotal spawn` on your own machine mints from your own signing material, so it can still\ngrant any channel you name: that is the out-of-band grant, not a way around the rule.\n\n**Failed turns publish run errors.** Claude Code decides for itself\nwhether a turn finished or died and fires one of two hooks accordingly, so the connector relays that\ndecision rather than making one of its own: a turn that ended on an API error ends its run with\n`RUN_ERROR` carrying the fixed message `run failed` and no code. Neither the detail Claude Code\nreported nor its error kind is published there: both are upstream values that can echo your prompt or\ntool output, and the events channel has a different read ACL. The error kind still reaches presence\nas the agent's condition (`rate_limit`, `auth`, `billing` and the rest). A turn that ended normally still\nends with a run-finished event carrying no outcome, which says the turn ended and does not claim it\nsucceeded.\n\nEvents are written to a per-session write-ahead log before they are published, so a hook that fires\nafter a restart resumes at the cursor it left rather than replaying or skipping, and a run that was\nopen when the session stopped is closed rather than left dangling.\n\nA delayed initial connection or a broker reconnect pauses event publication without closing MCP.\nThe recorder waits for both the mesh binding and its transport; unpublished records stay in the\ntranscript until the connection recovers. Stopping the connector cancels that wait. A recording error,\nincluding a conflicting transcript or damaged event state, stops the publishing plane and not the\nsession: it is reported with the delay before the next re-establish, on every space, and the seat keeps\nits mesh connection. A second transcript path offered to a bound recorder is refused the same way, and\nthe first path stays bound.\n\nOne channel carries **every session of one agent**, because it is named after the principal and not\nafter the session. Alongside the per-session logs the connector keeps one small record per principal,\nholding the last sequence the broker assigned on that channel, so a new session continues the stream\nits predecessor left instead of starting again from nothing. Both live under the events state root\n(`COTAL_WORKSPACE_ROOT`), and neither is something you edit by hand.\n\nA **missing** record is not a fault: the connector rebuilds it from the session logs beside it,\nwhich is how an agent that was already running before this record existed keeps its stream. That\nrebuild stops if any one of those session logs is damaged. Unreadable, not valid JSON, and written\nfor a different principal all count, and so does a session directory or a log that is a link rather\nthan the real file the connector wrote, or a log that has more than one name. A tip taken from the\nrest would be too low, and it would stop publication later with nothing left to point at the cause.\nThe connector names the file instead, and the only way past it is the directory removal described\nbelow, under the same condition. A record that **disagrees with the broker** is a fault, and the\nconnector stops publishing and says why rather than guessing. A record that **moved while a session\nwas writing to it** is refused the same way: it means something else wrote the principal's record,\nand the connector reports which value it held and which the file holds rather than writing over the\nlater one. There is no command to clear it. The state is the principal's directory under the events\nroot, and clearing it by hand means removing that directory whole: the sequence, the cursor and the\nper-session logs only mean anything together, so removing part of it leaves a state the next start\nrefuses. Removing it is only half a remedy, and the half that comes first is the channel. The\ndirectory is where the agent's memory of the tip lives, not the tip itself, so on a channel that\nstill holds frames the next session opens expecting an empty one and stops on the same\ndisagreement, with the logs a tip could have been rebuilt from now gone. Purge the channel first,\nthen remove the directory.\n\nReading it: `cotal console` and the web console draw event frames directly. A frame carries no text\npart by design, so a surface that renders a message as flat text shows a marker instead of prose.\n\n**On a per-user-auth mesh, the default event plane needs the spawner's grant to cover the channel.** The event\nchannel is added to the child's publish set, and delegation only narrows: an agent may hand down\na subset of what it holds and no more. So a peer-initiated spawn is refused unless the\nspawning identity's own grant already covers the child's event channel. The refusal prints the\nexact `cotal actor grant` command that widens it. An operator launch, whose chain reaches an\nadmin-scoped or roster row, is unaffected. Passing `events: false` is the explicit opt-out.\n\nArming the event plane through a typed spawn request (`manager.spawn` with `events`, including\nthe CLI's `cotal spawn --detach --events`) is allowed on a user mesh when the child runs under\nthe caller's owner. This also satisfies a registration policy that requires the plane. A spawn\nunder another owner still needs the caller's admin tier. Without it, an explicit request or a\nrequired plane is refused before provisioning. A silent non-owner caller in a space without that\npolicy has the plane disarmed with a notice in a successful reply. Later provisioning can still\nrefuse the spawn. The child's own-channel rule and the ledger's delegation envelope still apply.\n\n## Resume a session\n\n`--resume <session-id>` pulls an existing Claude session, its context and transcript,\ninto the mesh. It **forks**: Claude mints a *new* session id from that transcript\n(`--resume <id> --fork-session`), so the meshed agent gets its own session and the\noriginal is untouched.\n\n- `cotal spawn --resume <id>` (foreground) is the primary surface: the transcript is on\n *your* machine, and errors are Claude's own stderr, inline.\n- `--detach --resume <id> --on <instance>` carries a session held on *your* machine to that\n manager instance, which may run on another host. The CLI finds the transcript under your\n Claude config (`~/.claude`, or `$CLAUDE_CONFIG_DIR`), sends it through a JetStream Object\n Store bucket only that instance reads, under a writer credential pinned to that one transcript,\n and prints `carried session <id> to <instance>:\n sha256:<hex>, <sent> of <size> bytes sent in <chunks> chunks`. Without `--agent`, the CLI first asks that\n instance which harness the persona resolves to, so a persona with `agent: claude` is carried even\n when your `COTAL_DEFAULT_AGENT` names another connector. A re-run of the same bytes\n sends nothing, and an interrupted carry continues where it stopped. The seat forks it in a\n private Claude home under the manager's `.cotal/seat-homes/`, which no other seat's Claude\n lists or finds, and which is removed when the seat stops. When Claude starts the fork, the seat\n records the SHA-256 of the transcript it read from its own project; the manager stops a seat\n whose record names other bytes than the carried ones, or that records none within the join\n timeout after it joins, an uncertain launch included, and otherwise shows that record as the\n seat's provenance. `cotal attach` to such a seat names its source after the seat\n name, as `(resumed from <host>:<id>)`. A remote manager receives a carry when its host issues it a\n transfer reader. On a user-auth mesh the CLI exchanges the operator's login for a one-object\n `transfer-writer` view, which needs scope `admin`.\n- A session name in place of an id is refused, listing each session on this host that carries\n that name with its id, SHA-256 and modification time. An id this host does not hold resolves\n against the **manager host's** `~/.claude`, as before.\n- A seat-private home holds no login. The manager host needs `CLAUDE_CODE_OAUTH_TOKEN` (from\n `claude setup-token`), `ANTHROPIC_AUTH_TOKEN`, or a cloud provider selection in its\n environment; `ANTHROPIC_API_KEY` alone is refused. The private home trusts the authorized\n launch directory without changing the manager's home. Claude must be 2.1.234 or later.\n- The manager waits for a real outcome: `\u2713 started` means the agent *joined the mesh*,\n `\u2717 exited on launch` carries Claude's last output and the seat's exit diagnostic, such as a\n startup confirmation that never appeared, and an uncertain launch (~30 s) is reported without\n tearing the agent down.\n- `--opt resume=` with no id passes a bare `--resume`, which opens Claude's session picker, and the\n agent joins the mesh once you pick a session. A managed launch with it is ready once Claude runs:\n the CLI says the agent is waiting for you at Claude's session picker, attaches you to it in a\n terminal, `-d` included, or otherwise prints the attach command, and exits 0. `--opt resume=<id>`\n passes `--resume <id>` and joins as any launch does.\n- Resume is an **operator surface only**, deliberately not exposed on MCP `cotal_spawn`\n (a mesh peer naming host-local transcripts would widen `spawn` into transcript\n disclosure). Claude, Hermes, Jcode and pi support it, each by forking; OpenCode and Codex fail\n loud ([Connectors](connectors.md)).\n- Needs a `claude` new enough for `--resume \u2026 --fork-session` (verified on 2.1.197).\n\n## Sharing your MCP servers\n\nA spawned session loads the MCP servers your normal Claude Code configuration enables on the\nlaunch host: user, local, project, plugin and connected-account sources. Claude handles discovery,\nprecedence, disabled servers, project approval and managed policy. Cotal adds its mesh server\nand any overrides declared in `connectors.claude.mcpServers` ([format](config.md)). It does not\ncopy native server definitions or credentials into the launch file or onto another machine.\nThe selected `CLAUDE_CONFIG_DIR` remains in effect for an ordinary local spawn.\n\nFirst-run setup still seeds eligible user-scope declarations into the Cotal config and preserves\nan existing list. That list is not the source of truth for native discovery. A same-named declared\noverride takes precedence; changing the native definition does not rewrite an override you kept.\nAn empty Cotal sharing map leaves native discovery enabled.\n\nUse `--share-tools tavily,figma` to load only those declared servers plus Cotal, or\n`--share-tools none` for Cotal alone. These explicit selections use `--strict-mcp-config`.\nCarried sessions also stay strict and keep their seat-private home; operator profiles and\nconnected-account credentials are not copied into them. Native managed policy still applies.\n\nThe child environment stays allowlisted. References in declared shared servers and the selected\nuser-owned MCP configuration forward their named variables. Repository and plugin files cannot\nadd ambient secret names merely by referencing them; supply such names through an explicit\nshared declaration or the existing `spawn.env` policy. Native files retain their own configured\nvalues. Documented native MCP switches, including the connected-account opt-out and tool-search\nchoice, keep their explicit values. Cotal does not forward the entire parent environment or force\ntool definitions to load upfront.\n\nSharing a credential grants it to the session, so enable only servers the session may use.\nEach enabled server may start a process per seat; an explicit sharing selection can reduce that\ncost. Native discovery does not change Cotal's mesh identity, hooks or channel wake path.\n\n### One Cotal path per session\n\nA spawned session is on its mesh through the local Cotal server. A connected-account source can\nalso give it a hosted Cotal connector, which would join the same session a second time under\nanother name. The plugin's `PreToolUse` hook refuses every `cotal_*` tool of any server other than\nthe local one in a session Cotal launched, with the reason in the refusal. It decides in the hook\nprocess, so the refusal holds when the control socket is down. A Claude Code session that Cotal did\nnot launch keeps the hosted connector. Where the plugin is installed, its local server's\ninstructions tell the model to use the local path unless you ask for the hosted one.\n\nIn such a session the plugin also keeps each hosted connection with the session that opened it. A\n`PostToolUse` hook on the hosted `cotal_connect` records the returned `connection_id` under the\nplugin's data directory (`CLAUDE_PLUGIN_DATA`), and `cotal_disconnect` releases it. A fork made with\n`--fork-session` inherits that id in its transcript but has a new session id, so its calls with the\nid are refused and it is told to call `cotal_connect`. A resumed session and its subagents keep the\nsession id and pass. An id the plugin never saw opened goes to the hosted connector, which checks\nthe account.\nThe guard runs only where the `cotal` plugin is installed. A Claude Code session with only the\nhosted connector has no hook, so nothing records its connections and its forks are not refused.\n\nA hosted connector cannot start a turn, so a hosted conversation hears a message only when it calls a\ntool. `cotal_wait` holds until a message that would wake the agent arrives and returns it, or returns\nwith none after the service's bound. The conversation calls it again to keep listening. Every other\nhosted tool adds a line to its result when messages are waiting. A conversation with no turn running\nis not woken.\n\n## Host sessions in one process\n\nA host serving many managed seats can run their MCP servers in one process with the subpath\n`@cotal-ai/connector-claude-code/mcp`, which loads with both `import` and `require`:\n\n```ts\nimport { serveClaudeSession } from \"@cotal-ai/connector-claude-code/mcp\";\n\nconst session = await serveClaudeSession({ env, input, output });\nawait session.close();\n```\n\nLoading the module only exports it; it serves its own stdio only when run, as `node dist/mcp.cjs`.\nThe subpath ships CommonJS declarations so TypeScript hosts can use either module format,\nincluding CommonJS consumers with Node16 or Node18 module settings.\nClaude Code pushes `claude/channel` only to a stdio server it launched, so each `claude` still\nlaunches a stdio command, such as a small relay that pipes its stdio to the host.\n\n- **Managed only.** `env` is a complete launch environment, copied when the call starts. It must\n carry a mesh identity and the full control pair, or the call rejects before the session joins the\n mesh. The presence environment label, bearer commands, carried-fork records and feedback contact\n lookup use that copy. Feedback's Git fallback reads the session's global config, including its\n includes, without reading the host repository config. Set `COTAL_FEEDBACK_EMAIL` or pass the\n feedback tool's `email` argument to choose another contact. Include the `PATH`, `HOME` and other OS\n variables subprocesses need; the host's environment is not merged into them. Subprocesses still\n use the host process's working directory. A session's card does not: its `meta.cwd` is the `cwd`\n its own `SessionStart` hook reports on startup, resume or fork, absent until then, and unchanged by\n clear or compact. It is advisory display data that every reader of the space's roster can see. A plain `claude` with no identity is served by `mcp.cjs` run directly. Sharing a process\n does not isolate untrusted tenants or their files.\n- **Streams.** `input` carries JSON-RPC from the client and `output` carries this session's replies\n and `claude/channel` pushes. The caller owns both streams. Input that has emitted `end` or is\n destroyed rejects startup. Buffered unread input remains eligible. When an active session's\n `input` emits `end` or `close`, the session closes, including after a relay connection resets.\n Teardown may pause `input` and removes the transport's error handler. A host that reuses the\n stream must resume it as needed and keep its own stream error handlers.\n- **`fatalBind`.** Off by default: a control socket that cannot be bound rejects the call and the\n host keeps its other sessions. Only a process that is the session turns it on, as `mcp.cjs` does.\n- **Teardown.** `close()` closes the control socket, the wake policy, the AG-UI emitter, the mesh\n endpoint and the MCP server, and is idempotent. It removes its input listeners without closing\n the caller's streams. The returned promise rejects if teardown fails. A startup failure closes\n what was started before the call rejects. A manager shutdown frame or input EOF or closure closes the\n session and then calls `onShutdown(code)` once. A teardown failure is logged and reported with\n code 1, without becoming an unhandled rejection in the host. Close waits for a pending broker\n dial to settle. A dial that fails after shutdown starts is not a teardown failure. The session\n releases local inbox custody only after its connections are proved closed, so the same lifecycle\n can reopen after a successful close. An active duplicate or an unproved close retains custody.\n\n## Feedback\n\n`cotal_feedback` works out of the box: without a key it posts to the public intake at\n`https://cotal.ai/v1/feedback` (needs a contact email: `COTAL_FEEDBACK_EMAIL`, then\n`git config user.email`, else the agent asks). Set `COTAL_FEEDBACK_KEY=fbk_<key>` in a\nbeta tester's environment to route to the keyed intake (`Authorization: Bearer`, identity\nderived from the key); `COTAL_FEEDBACK_URL` overrides either endpoint. The CLI can send\ntoo: `cotal feedback \"<summary>\" [--type bug]`. Each submission carries\n`origin: human | agent`, whether the tester asked, or the agent auto-reported a major\nissue.\n"
|
|
65928
65928
|
},
|
|
65929
65929
|
{
|
|
65930
65930
|
"slug": "connect-codex",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cotal-ai/pi",
|
|
3
|
-
"version": "0.105.
|
|
3
|
+
"version": "0.105.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cotal",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"@earendil-works/pi-tui": ">=0.79.10"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@cotal-ai/core": "0.105.
|
|
38
|
-
"@cotal-ai/connector-core": "0.105.
|
|
37
|
+
"@cotal-ai/core": "0.105.1",
|
|
38
|
+
"@cotal-ai/connector-core": "0.105.1",
|
|
39
39
|
"@cotal-ai/smoke-kit": "0.0.0",
|
|
40
40
|
"@earendil-works/pi-ai": "0.79.10",
|
|
41
41
|
"@earendil-works/pi-coding-agent": "0.79.10",
|