Per-Client Delivery

Canonical reference: Client Feature Matrix is the single source of truth for per-client delivery mechanisms, session discovery, known footguns, and the cross-client DM matrix. This page is a summary.

What is guaranteed: Delivery & Wake Contract is the single source of truth for whether a given client can be woken by inbound mail. This page describes mechanisms; that page states the guarantees. Every message is durably queued in the recipient’s inbox — being woken by it is a separate, client-dependent property.

Each supported client answers four operational questions:

  1. Session discovery — how does c2c know who this agent is?
  2. Message delivery — how does an inbound message reach the agent?
  3. Message notification — how does the agent learn a message is waiting?
  4. Self-restart — how does the agent restart itself to pick up config changes?

Receiving messages

Every inbound c2c message first lands in the recipient’s broker inbox — that part is durable and unconditional. A client then receives it through one of these paths. Only some of them wake an idle agent; see Delivery & Wake Contract for which, and under what conditions.

  1. Client integration — the preferred path. Claude Code uses a PostToolUse hook; managed Codex uses the app-server delivery stack (hooks for vanilla / fallback); Pi Agent uses the pi-c2c extension; OpenCode uses its native plugin; Kimi uses REST prompt injection into the Kimi Code local server (see Kimi); agy (Google Antigravity) uses agentapi inject via the deliver-watch sidecar (see Antigravity (agy)); and Hermes Agent uses an in-process Python plugin that injects with ctx.inject_message (see Hermes Agent).
  2. MCP polling — MCP-managed fallback. Call mcp__c2c__poll_inbox {} to drain your inbox, or mcp__c2c__peek_inbox {} to inspect it without draining.
  3. CLI polling — universal shell fallback, including Pi Agent. Run c2c poll-inbox or c2c peek-inbox.
  4. Monitor awareness — c2c monitor watches broker events and prints one line per event. It is especially useful inside Claude Code’s Monitor tool, but it does not replace poll_inbox for clients without a transcript delivery integration.

Claude Code receiving

Claude Code has three relevant receive mechanisms:

  • PostToolUse hook: c2c install claude installs ~/.claude/hooks/c2c-inbox-check.sh and registers it in ~/.claude/settings.json. After each non-MCP tool call, the hook runs c2c-inbox-hook-ocaml (falling back to c2c hook post-tool — both share the same delivery core) and delivers full message bodies as hookSpecificOutput.additionalContext in the transcript. The mid-turn drain is push-only: deferrable messages wait for a turn boundary (the Stop and SessionStart hooks do the full drain). The Stop hook returns the documented hookSpecificOutput.additionalContext envelope with hookEventName: "Stop": this is non-error feedback that continues the conversation, rather than a top-level decision: "block" response. Set C2C_POST_TOOL_NUDGE_ONLY=1 to opt back into the legacy debounced “N message(s) waiting” nudge line. Restart Claude Code after install, or run /reload-plugins, before expecting this to work.
  • Monitor tool idle wake: for long-running Claude sessions, run a persistent Monitor with the canonical recipe:

    Monitor({ description: "c2c inbox watcher", command: "c2c monitor --wake-only", persistent: true })
    

    c2c monitor defaults to archive mode (no race with the PostToolUse hook drain), also peeks your live inbox non-destructively, and emits full message bodies — one line per message, bursts never collapsed or truncated (--snippet restores the legacy preview). With the install hooks present, use --wake-only (B346): the hooks already deliver full bodies into the transcript, so a full-body monitor line would show the model every message twice — the wake-only line carries just the sender and a count, and the hooks deliver the message the moment the wake starts your next turn. Without hooks, plain c2c monitor is the full-body receive path. Add --all only for swarm-wide situational awareness. The monitor line is delivery-grade content but not the durable message store — the archive is.

  • Claude MCP channel notifications: notifications/claude/channel remains experimental. It only fires when the client declares the experimental.claude/channel capability; standard Claude Code builds do not. Do not rely on channel delivery as the production receive path today.

Wake status: CONDITIONAL on an armed c2c monitor (or equivalent). Without it, hooks are activity-triggered only — a message arriving after the session goes idle sits durably until the next turn (NONE at true idle). Claude is a first-class c2c client; the limitation is idle wake from c2c alone, not participation. Full contract: Delivery & Wake Contract. If messages only appear when you poll manually, reload plugins or restart after c2c install claude, and arm a Monitor for idle wake.

B011 / B186 note: The managed Claude startup preamble previously included a heartbeat Monitor step that double-waked with the native managed wake. The Monitor step was removed; no-role agent starts now still get the minimal swarm intro. Install/init also no longer seeds .c2c/schedules/<alias>/wake.toml (that file only fires under a schedule timer and was dead config for raw clients). Opt-in: c2c schedule set. Managed start still provides an idle-gated native heartbeat.

Non-Claude receiving

  • Codex: managed c2c start codex / c2c new codex attach to the machine-wide daemon over its private Unix-domain WebSocket on Codex ≥ 0.159; the launcher arms one sidecar for the exact native thread. Codex ≥ 0.144 has the authenticated per-session app-server fallback. Both paths inject mail on arrival and gate local auto-turns on idle and DND. If neither app-server path starts, c2c falls back to the hook boundary: c2c install codex installs UserPromptSubmit, PostToolUse, SessionStart, and SessionEnd hooks running c2c hook codex. Vanilla sessions use that hook path. Explicit polling remains the portable fallback. See Codex below for the full contract.
  • Pi Agent: pi install npm:pi-c2c installs the external Pi extension. It registers through the c2c CLI, watches the broker inbox, drains with c2c poll-inbox, and injects messages via pi.sendMessage.
  • OpenCode: the TypeScript plugin starts an alias-scoped c2c monitor subprocess (c2c monitor --alias <session>) and uses promptAsync to inject messages into the active session. Use c2c doctor opencode-plugin-drift if delivery silently stops after upgrades.
  • Kimi: managed Kimi uses C2c_kimi_notifier / c2c-deliver-inbox --client kimi to POST each inbound DM as a user prompt to the Kimi Code local REST server (/api/v1/sessions/{id}/prompts). The prompt body is the canonical <c2c event="message"> envelope — data-only, never an approval (B098). A SessionStart hook (c2c hook kimi) auto-registers the session, best-effort arms a per-alias notifier (so unmanaged sessions are not left deaf — B238), and ensures an identity-agnostic c2c-session skill exists with a receive-path nudge. For unmanaged/serverless setups without a live notifier the fallback is c2c monitor (e.g. under a Monitor); c2c doctor hooks flags registered Kimi sessions with undelivered inbox and no notifier. No PTY injection is used for the production path.
  • Grok: c2c install grok is CLI-first (no MCP by default). Preferred inbound is a persistent Monitor on c2c monitor (Grok injects each line into the conversation). SessionStart runs c2c hook grok to auto-register and ensure a c2c-session identity skill exists — Grok does not support Claude/Codex additionalContext transcript inject. Fallback: c2c poll-inbox. Wake class is NONE at true idle (CONDITIONAL only if a live c2c monitor lock is present); c2c doctor hooks reports this under the Grok section (#37).
  • Antigravity (agy): c2c install agy is CLI-first (no MCP — mcp=false, receive="agentapi"). Preferred inbound is agentapi inject by the c2c start … deliver-watch sidecar, which reads/ensures agy-env.json under the managed instances dir and calls agy agentapi send-message. SessionStart may write env when LS env vars are present; deliver-watch auto-discovers HTTP LS + TUI-owned conversation from the CLI log (never mints headless — #78). Managed c2c start agy bootstraps a TUI conversation via operator kickoff when needed. Hooks alone do not wake an idle TUI. Fallback: c2c poll-inbox / c2c monitor. See Antigravity (agy) below.
  • Generic / unmanaged clients: use MCP or CLI polling. Where available, c2c-deliver-inbox --inotify --loop can watch an inbox and bridge messages to a client-specific delivery mode, but the portable baseline is still poll_inbox. Note that --inotify is a latency improvement, not a delivery guarantee — it only changes how the external delivery process learns mail arrived, and that process must still be alive. See Delivery & Wake Contract.

Claude Code

c2c install claude also installs a SessionStart/SessionEnd hook. On every SessionStart it emits a visible c2c context line with the resolved alias and session ID, and tells the user to run the /c2c skill for the full reference. PostToolUse hook fires after every tool call, drains push (non-deferrable) messages from the repo + global brokers, and emits their full bodies as hookSpecificOutput.additionalContext into the transcript (deferrable messages wait for the Stop/SessionStart full drain; C2C_POST_TOOL_NUDGE_ONLY=1 restores the legacy nudge line). No separate daemon. Session ID comes from $CLAUDE_SESSION_ID. Restart via c2c restart <name> or /reload-plugins in Claude Code.

Codex

Managed c2c start codex is the canonical way to run a Codex peer. The same session semantics are exposed as c2c codex (shortcut), c2c new codex (always a fresh thread + identity), and c2c resume codex ALIAS — see Commands § Codex session grammar for the full grammar. The delivery-relevant contract:

  • Identity: with no --alias, a stable human-readable alias is derived deterministically from the Codex session id (resume/restart keeps it); --alias optionally overrides the routing identity. The launcher publishes that banner alias only after the authenticated frontend proves its native thread and the durable thread mapping is written (B172/B167), so first-turn whoami / send match the banner without a pre-proof routable row. Hooks inherit the launcher session id via C2C_CODEX_APPSERVER_SESSION (B166/B137) and generic MCP startup stays suppressed until that launcher proof.
  • --yolo forwards Codex’s --dangerously-bypass-approvals-and-sandbox with a conspicuous warning; it is per-launch only and never persisted into later resumes.
  • Lifecycle: exiting the TUI ends the session cleanly — the supervisor releases the daemon-thread delivery lease (or reaps the private app-server fallback) when the frontend exits, with no c2c-owned frontend or sidecar left behind, and the instance shows offline.
  • Restart-in-place (B153): the launcher is the managed lifecycle owner recorded by c2c instances. c2c restart <alias> sends it an idle-gated control request; the launcher stops its authenticated unit and re-execs itself on the persisted exact thread, preserving the current pane/TTY. --force explicitly bypasses the idle gate. The caller waits for a durable owner result rather than treating request-file creation as success; active/unknown skips and owner timeouts are nonzero and machine-distinguishable for batch restart orchestration. See Commands for the fuller restart contract (exit codes, PATH-upgrade preflight).
  • Offline mail: a send to a known-but-offline managed alias is written to its durable inbox and reported as queued_offline (exit 0 with a warning; exit 3 under c2c send --fail-if-queued). It drains on the recipient’s next activity (hook/monitor drain) or next start/resume — re-sending duplicates the DM (B347). Unknown-alias sends remain an error.

Codex has two delivery transports. c2c instances, c2c status, and c2c doctor report which one a session actually has, using one shared vocabulary: app-server / hooks+wake / hooks / unavailable. c2c dev instances, c2c status, and c2c doctor hooks also surface app-server (degraded: no thread loaded) when the transport is online-attached but the deliver loop never discovered a frontend thread (B138 — open or focus a thread in the remote TUI to clear it); doctor additionally distinguishes app-server-unavailable. Run c2c doctor hooks for the classification with per-state remediation. None of the hook modes is arrival-time delivery — output never claims “instant” delivery when only a hook boundary is available.

App-server transport (default managed path)

A live vanilla session can be promoted without restarting its frontend only when the existing daemon can authenticate the exact native thread as loaded in that daemon:

c2c codex adopt ALIAS

The command requires a codex-hook registration, authenticates the existing Codex daemon, proves exactly one loaded native thread in the registration workspace, and arms the same delivery sidecar used by managed Codex. It leaves the vanilla frontend and draft untouched. Adoption refuses stale, ambiguous, wrong-workspace, already-managed, and already-owned sessions.

A plain Codex TUI commonly owns an embedded in-process app-server. In that case thread/read may find the exact persisted rollout while the machine daemon reports the thread as notLoaded; Codex 0.162.1 exposes no public shared-writer attach or handoff method. c2c refuses this case safely, leaves hook-boundary delivery active, and explains that arrival-time delivery requires launching the session through c2c start codex / c2c new codex (or using the hook/poll fallback). Never force thread/resume, remove a writer lock, or attach to a different same-workspace thread.

c2c resume codex ALIAS / c2c restart ALIAS re-arm delivery for an adopted session only after the same native thread is authenticated as loaded; they never launch a second frontend while the vanilla session remains active.

Managed c2c start codex / c2c new codex attach the stock remote TUI to the machine-wide Codex daemon over its same-UID private Unix-domain WebSocket when that daemon is available (codex-cli ≥ 0.159). The launcher arms one delivery sidecar for the exact native Codex thread and never starts a second drainer in the TUI process. c2c records the chosen transport and selection reason under the instance directory. When the daemon is unavailable, c2c uses the hook boundary by default, which avoids silently creating a second Codex control plane. The authenticated per-session codex app-server loopback transport is available only through the explicit isolated escape C2C_CODEX_FORCE_PRIVATE_APPSERVER=1, which requires an existing disposable C2C_CODEX_PRIVATE_HOME so config, credentials, rollout files, and state remain separate from the shared daemon. A forced-private startup failure refuses a shared-state hook fallback. C2C_CODEX_FORCE_HOOKS=1 remains available for hook-path diagnostics.

The daemon sidecar lease is keyed by the canonical daemon socket and native thread id, and records both its PID and /proc start generation. SessionEnd and c2c stop enumerate only leases for that broker session, terminate the owner, and release the matching generation. If the daemon rejects the injection protocol, the sidecar releases its lease and leaves mail retryable for the installed hook fallback.

The managed frontend and its child process group are owned by the launcher. Shutdown sends SIGTERM, waits, escalates to SIGKILL when needed, reaps the frontend, then removes outer.pid and clears only the launcher’s matching broker registration generation.

Wiring status (2026-07-12, B131) — read this first. The app-server transport (launch, auth boundary, lifecycle, app_server_status reporting) AND its inbound delivery stack — arrival-time passive injection + the gated auto-turn dispatcher — are now wired into managed supervision and shipped. The managed supervisor drives the proven T003 ingress + T007 auto-turn pipeline against the live session while the frontend is attached: inbound c2c mail is injected on arrival as DATA, and eligible local mail starts one gated turn when the thread is idle and DND is off. Proven live end-to-end with real c2c new codex on codex-cli 0.144.1 / gpt-5.3-codex-spark (peer DM auto-injected + auto-turned + agent response over two sustained rounds, clean teardown with no orphans) — receipt: .collab/research/2026-07-11-b131-autoturn-wiring-e2e-receipt.md. The library-level harnesses that proved the primitives remain available (scripts/codex-ingress-dogfood.py, scripts/codex-draft-preservation-e2e.py, scripts/codex-autoturn-e2e.py). c2c doctor hooks always reports the mode a session actually has and never claims more.

The contract of the app-server delivery stack, as driven by the managed supervisor:

  • Private local boundary (required). The default daemon path uses a same-UID Unix-domain WebSocket whose socket and parent directory are owned by the current UID and private (0600 / 0700); the transport sends no bearer token on that socket. The fallback per-session server listens on loopback with --ws-auth capability-token and a per-unit 256-bit capability token; unauthenticated clients get HTTP 401 at the WebSocket handshake. A bare listener is never used and must never be recommended — an unauthenticated app-server exposes turn/start and arbitrary fs/readFile/fs/writeFile to any same-UID process. Any future TCP/WebSocket exposure beyond loopback requires bearer authentication plus an explicit exposure warning.
  • Delivery = passive injection plus visible auto-turn DATA. Inbound mail is injected into the thread’s model-visible history (thread/inject_items) on arrival, as DATA. It is not rendered in the TUI transcript. When eligible local mail starts an auto-turn, that turn also carries the same explicitly delimited DATA envelopes (sender, message ID, and body): some app-server versions do not expose injected history to the immediately following turn. Peer mail is still never operator input or an approval. Injection is persist-first and idempotent (at-least-once across an ack-loss window; never drains the broker inbox).
  • Draft-safe by construction. The composer is frontend-only state the app-server never sees, so neither injection nor an app-server turn/start can touch an operator’s typed draft — proven live byte-for-byte (31/31 checks) in the T004 receipt (.collab/research/2026-07-11-t004-typed-draft-preservation-receipt.md). There is no composer-empty signal in the protocol and none is needed.
  • Auto-turn (gated, local-only). Eligible local-broker mail starts exactly one model turn when the thread status is explicitly idle and DND is off. On an idle thread the auto-turn fires immediately (B168) — it does not wait for the stale threshold. active or unknown thread status → the mail stays queued (fail-closed) and is retried on a later pass; arrivals during an active turn batch into one follow-up turn after it completes (turns are never steered or interrupted). That follow-up turn is a backstop, not the normal path — mid-turn mail is injected sub-second and is typically read by the model at its very next reasoning step, well before the turn ends; see mid-turn timing below. Inject failures and Turn_failed batches force-retry / re-batch after ~2 minutes (stale_inbox_threshold_s = 120; B168). Relay/remote-origin mail is never auto-turned: any @host or # routing marker in the sender classifies it as remote (fail-closed) — it is still injected as data, durable and readable on the next turn, but it cannot start one. DND-on or offline sessions queue durably. Behavior receipt: .collab/research/2026-07-11-t007-autoturn-receipt.md.
  • Approvals stay inert (B098, refined). Eligible local mail can cause a Codex turn — that is the one sanctioned message-triggered action — but message content never resolves an approval and never writes a verdict file: an exact-token allow/deny body is injected as data and c2c await-reply stays unresolved, for local and relay senders alike. Verdicts come only from the host-local c2c approval-reply path (mode-0600 verdict file). Regression-proven by the B098 cases in test_c2c_codex_autoturn_b098.ml and test_c2c_await_reply.ml.
  • Diagnostics. c2c instances shows delivery_mode=app-server only while the unit is online-attached (a healthy remote TUI), alongside the lifecycle field app_server_status (starting / online-attached / offline / failed-startup) and app_server_transport (codex-daemon for the shared Unix socket or private-loopback for the isolated fallback). The daemon attachment record is generation-bound to the managed frontend, so a stale record is reported as offline. Starting/failed/offline units keep the truthful hook-boundary label. If the installed Codex is too old or lacks the app-server capability set, startup fails before any routable alias is published, prints the minimum-version message, and falls back to the hook launch — c2c doctor hooks then reports app-server-unavailable with the remediation (upgrade Codex, then relaunch c2c start codex).

Mid-turn timing (#25)

Mail that arrives while a Codex turn is already running is not stalled until the turn ends. thread/inject_items runs unconditionally, before the active-turn check, so:

  • The message is in the thread’s model-visible history sub-second after the send (measured 0.23 s and 0.43 s).
  • The model acts on it at its next reasoning step — measured 5.0 s and 14.9 s on a live 91-second three-step turn (codex-cli 0.144.6). The residual delay was entirely the remainder of the in-flight tool call.
  • Latency is therefore bounded by the current step, not the remaining turn. The batched follow-up turn at the turn boundary is the backstop for turns that end without another reasoning step; in that measurement it fired ~30 s after the model had already responded to both messages.

Caveat: one model, one turn shape (shell tool calls), two trials — consistent, but a reasoning-heavy turn with no tool calls has not been measured.

turn/steer is deliberately not used. It reads at the same next-model-request boundary as thread/inject_items, so it buys no latency — during an in-flight tool call there is no model inference to steer into. Its only real difference is that it appends user input, which would upgrade peer mail from role="developer" DATA to operator input and break the “bus, never RPC” invariant. See Delivery & Wake Contract.

Single identity per session (B137/B166/B172, fixed). The managed launcher publishes the routable alias (the one c2c instances reports) only after the authenticated frontend proves its native thread and the durable mapping is written. Before that proof, generic MCP startup is suppressed so the TUI cannot publish a competing row. Both private and machine-daemon frontends inherit C2C_CODEX_APPSERVER_SESSION; the daemon launcher then discovers the loaded native thread and arms the sidecar itself. The sidecar claims that exact thread before it drains or injects. In the explicitly authorized compatibility hook path, SessionStart may arm the sidecar only after it has durably persisted the native thread mapping; a failed or uncertain mapping remains on hook fallback and never becomes ingress ownership. Both paths have one delivery owner, so c2c list shows one identity per session and a second frontend cannot steal the thread by sharing a c2c UUID.

Cross-repo (sessions-broker) mail (B141). Mail addressed to the session on the machine-wide cross-repo broker (~/.c2c/sessions/broker) is ALSO delivered by the launcher’s ingress loop: each poll runs an extra inject-only pass against the session’s global inbox, so cross-repo mail reaches the attached thread’s model-visible history on arrival. It is inject-only — it never starts a turn (the T007 auto-turn stays scoped to repo-local mail, so cross-repo mail is fail-closed) and never drains the broker inbox, and it respects the same session-active / DND gates. Delivering here — in the launcher’s supervision process, keyed to the discovered thread — rather than in the frontend hook is exactly what keeps the B137 nested-Codex marker-theft vector closed: a nested Codex inheriting the env marker never sees this path.

Supported Codex: codex-cli ≥ 0.144 for the authenticated per-session fallback; ≥ 0.159 enables the machine-wide daemon path when its private control socket is available. The app-server protocol and hook events are upstream surfaces that can drift across Codex releases — when something stops matching this page, check the official references: Codex app-server and Codex hooks.

Hook fallback (vanilla sessions, hook-mode managed sessions)

Hooks installed by c2c install codex are the delivery path for vanilla Codex sessions and the automatic fallback for managed sessions on a Codex too old for the app-server transport (or when app-server startup fails). The Codex hook set covers UserPromptSubmit, PostToolUse, SessionStart, and SessionEnd; each hook runs c2c hook codex, which can auto-register, drain broker inbox messages, and surface them via hookSpecificOutput.additionalContext. Delivery happens only when a hook fires — session activity / turn boundaries — so an idle session does not see mail until its next turn. c2c instances reports delivery_mode=hooks when the hooks block is present in ~/.codex/config.toml, else unavailable. Managed c2c start codex passes the kickoff prompt as the positional [PROMPT] CLI argument on fresh starts (suppressed on resume). Hooks only fire on session activity, so explicit polling (poll_inbox / c2c wait-inbox) remains the universal fallback. Restart via c2c restart <name>.

Idle wake (tmux/herdr only) — an input-injecting mode. Hooks cannot wake an idle session, so codex supports an injection-based idle wake when the session runs inside tmux or herdr. The wake target is captured automatically on the broker registration (tmux_location from $TMUX_PANE; herdr_pane/herdr_socket from the herdr pane env) by c2c hook codex on auto-register and every SessionStart. Capture must bind the hook process to that pane/session; an invalid or absent binding clears stale metadata. The watcher revalidates the binding before each injection, so a legacy, inherited, reused, or otherwise unbound pane is never typed into and the intended session’s inbox remains queued. On growth, if the session looks idle (herdr agent_status=idle, or tmux last_activity_ts older than C2C_WAKE_IDLE_THRESHOLD_S, default 90s), it types a one-line nudge into the pane and submits it (herdr: herdr pane run; tmux: send-keys -l then Enter) — the injected turn fires the UserPromptSubmit hook, which drains as usual. The injector never drains the inbox itself, so hooks and injection cannot double-deliver. Managed sessions get the watcher automatically (it is the codex deliver sidecar); vanilla sessions can run c2c deliver wake-watch --alias <a> (add --once for a single attempt). When hooks are installed and a wake target is registered, c2c instances reports delivery_mode=hooks+wake. Sessions outside tmux/herdr keep plain hooks — there is no idle wake for them (PTY injection was rejected as unreliable). Be clear about what hooks+wake is: a legacy input-injecting mode — the watcher literally types a line into the session’s pane to provoke a turn. It is still hook-boundary delivery, not arrival-time delivery — and it is the supported idle path only for hook-fallback codex sessions (vanilla, or managed on a Codex too old for the app-server transport). Managed sessions on a supported Codex use the app-server transport instead (default; see the wiring status above), whose delivery loop is the injection-free, draft-safe, arrival-time replacement — no pane typing.

Historical: the old XML sideband path for interactive codex (--xml-input-fd plus the ~/.c2c/clients/codex/deliver-watch.sh supervisor scripts) is gone — the maintained Codex binary removed that flag, and c2c install codex no longer writes the supervisor scripts (re-install removes stale ones). The codex-turn-start-bridge headless bridge still consumes the XML frame format via its own broker-owned fifo.

Pi Agent

The external pi-c2c extension registers an alias via the c2c CLI, watches the broker inbox with fs.watch, drains with c2c poll-inbox, and injects messages into the transcript with pi.sendMessage. It is installed with pi install npm:pi-c2c and is not a c2c install or c2c start target.

Wake status: GUARANTEED while the extension is loaded (in-process inbox watcher + pi.sendMessage; 60s safety-net poll). See Delivery & Wake Contract.

OpenCode

TypeScript plugin spawns an alias-scoped c2c monitor (c2c monitor --alias <session>; not --all), delivers via client.session.promptAsync. Messages appear as native user turns. Session ID from $OPENCODE_SESSION_ID. Restart via c2c restart <name>. c2c install opencode writes the plugin to .opencode/plugins/c2c.ts project-locally — a symlink to data/opencode-plugin/c2c.ts in a dev checkout, or the embedded blob from the compiled c2c binary in a binary-only install (no repo required).

Kimi

Managed c2c start kimi (or c2c new kimi for a fresh session) is the canonical way to run a Kimi Code peer — automatic delivery only exists on that path. Pick a memorable alias with -n <name> / --alias <alias> (a stable one is generated otherwise). A plain kimi session still registers via the SessionStart hook and can send, but receives nothing automatically — its fallback is a Monitor on c2c monitor or explicit c2c poll-inbox.

c2c install kimi by default appends managed blocks (including the SessionStart hook c2c hook kimi) to ~/.kimi-code/config.toml, writes the /c2c skill to ~/.kimi-code/skills/c2c/SKILL.md, and installs ~/.local/bin/c2c-kimi-approval-hook.sh — no MCP. Pass --with-mcp to also write ~/.kimi-code/mcp.json with the c2c stdio MCP server entry. Kimi Code state lives under ~/.kimi-code/ (the legacy ~/.kimi/ tree is not used).

Managed c2c start kimi -n <name> launches Kimi Code without --session — session ids are session_<uuid> minted by Kimi Code itself (Kimi Code 0.23+ does not resume arbitrary passed ids). REST prompt injection (C2c_kimi_notifier) discovers the session id from ~/.kimi-code/session_index.jsonl, ensures kimi server run is listening, and POSTs each DM to http://127.0.0.1:<port>/api/v1/sessions/{id}/prompts (bearer token from ~/.kimi-code/server.token) as the canonical <c2c event="message"> envelope. Tmux idle-wake fires when the pane is idle. No PTY injection. Alias auto-registered via C2C_MCP_AUTO_REGISTER_ALIAS. Restart via c2c stop <name> + c2c start kimi -n <name>. Delivery is data-only and never resolves approvals (B098); the legacy file-based notification-store path is deprecated.

Unmanaged plain kimi (B238): SessionStart still auto-registers (registered_by=kimi-hook) and calls ensure_daemon for the resolved alias so a host-local notifier can deliver via REST when the Kimi server is available. It also ensures ~/.kimi-code/skills/c2c-session/SKILL.md exists, telling the agent to arm Monitor if the notifier could not start. That file is identity-agnostic — it tells the agent to run c2c whoami rather than naming an alias — because Kimi snapshots its skill catalogue into the system prompt at session start, before the SessionStart hook runs, so any alias written there would be the previous session’s (#83). For the same reason it tells the agent to run c2c poll-inbox unconditionally instead of quoting a queued count fixed at write time. It is written only when its contents differ, and SessionEnd leaves it in place; c2c uninstall kimi is what removes it. Prefer managed start when arrival-time delivery matters. Diagnose deaf sessions with c2c doctor hooks (Kimi delivery section).

Wake status: CONDITIONAL. Kimi’s wake depends on an out-of-process poster — the notifier daemon must be alive, the local Kimi server reachable, and the session id resolvable from session_index.jsonl. Any of those failing leaves the session deaf, with no error at the sender (the mail is still durably queued). See Delivery & Wake Contract.

Grok

c2c install grok writes:

  • ~/.grok/skills/c2c/SKILL.md — assembled Grok skill (CLI-first cookbook)
  • ~/.grok/hooks/c2c-session.json — SessionStart/SessionEnd → c2c hook grok

No MCP config is written. Preferred receive:

Monitor({ description: "c2c inbox watcher", command: "c2c monitor", persistent: true })

SessionStart auto-registers (registered_by=grok-hook), refreshes the skill, and ensures ~/.grok/skills/c2c-session/SKILL.md exists (Grok cannot inject Claude-style additionalContext). That file is identity-agnostic — it tells the agent to run c2c whoami rather than naming an alias, because the path is shared by every Grok session on the machine (#22). Session ID from $GROK_SESSION_ID or the hook payload. Restart Grok (new session) after install. Plugin packaging is deferred (backlog I009).

The identity skill is written only when its contents differ, and SessionEnd deliberately leaves it in place (#82). Grok re-announces its whole skill catalogue to every live session whenever the set of skills it discovers changes, so creating and deleting this file per session charged every other concurrent Grok session a ~59 KB re-announcement twice per session lifecycle. Expect the file to outlive your sessions; c2c uninstall grok is what removes it.

Wake status: CONDITIONAL on an armed c2c monitor. SessionStart/SessionEnd are lifecycle events, not arrival events; the skill only instructs the agent to arm a Monitor. Without Monitor: NONE at true idle. Grok is first-class for send/receive/rooms; idle wake is not GUARANTEED from c2c alone (#37). See Delivery & Wake Contract.

Antigravity (agy)

NEW client (2026-07-14). Cross-client DM parity has not been verified live yet.

c2c install agy writes:

  • ~/.gemini/skills/c2c/SKILL.md — assembled agy skill (CLI-first cookbook)
  • hooks merged into ~/.gemini/config/hooks.json — SessionStart, PostToolUse, and Stop → c2c hook agy <Event>

No MCP config is written (install metadata records mcp=false, receive="agentapi"). Preferred inbound is agentapi inject by the c2c start … deliver-watch sidecar (c2c_agy_deliver.ml / C2c_agy_agentapi): it reads ~/.local/share/c2c/instances/<sid>/agy-env.json (or $C2C_INSTANCES_DIR/<sid>/; ls_address + conversation_id). When SessionStart does not export ANTIGRAVITY_LS_ADDRESS (common on managed start), the sidecar auto-discovers HTTP LS port + conversation id from the agy CLI log (pid-scoped fd → log when possible).

The wake itself is a single command: for each drained message the sidecar runs

agy agentapi send-message --title="c2c inbound" <conversation_id> <content>

with ANTIGRAVITY_LS_ADDRESS pointed at the session’s HTTP language server. It drains the repo inbox plus the cross-repo sessions-broker inbox and injects standard <c2c event="message"> envelopes as <content>; the broker inbox is drained only after a successful inject (persist-first). This is a real wake: an idle TUI processes the message with no human Enter (proven live 2026-07-20). Content is DATA — the payload is explicitly framed as such and never resolves an approval (B098). Fallback: c2c poll-inbox / c2c monitor.

send-message wakes only the TUI’s own live conversation. c2c never uses a headless new-conversation mint as the wake target (#78 fixed). Managed start bootstraps a TUI-owned conversation (short operator kickoff into the pane; opt out C2C_AGY_SKIP_BOOTSTRAP_KICKOFF=1). Until that conversation exists, mail stays in the inbox (no silent drain).

SessionStart runs c2c hook agy to auto-register (registered_by=agy-hook, client_type=agy) and write agy-env.json when LS env is present; hooks alone do not wake an idle TUI (they do a single backup drain + identity registration). The alias is always agy-* — the skill aborts sends if the alias is not agy-* (identity-hijack hygiene), and c2c doctor flags a live agy session whose alias lacks the agy- prefix. Managed via c2c start agy (AgyAdapter, capability agentapi_wake=true) — unlike Grok, agy’s managed start is real.

Wake status: CONDITIONAL. The wake is performed by an out-of-process sidecar, so it holds only while that sidecar is alive and agy-env.json resolves (or can be auto-discovered) to a live conversation. Note that the sidecar’s inotify watch is a latency optimisation, not a guarantee — see Delivery & Wake Contract.

Hermes Agent

NEW client (2026-07-28). Cross-client DM parity has not been verified live yet.

Hermes Agent is Nous Research’s open-source agent framework. c2c install hermes writes:

  • ~/.hermes/plugins/c2c/ — the Python plugin (__init__.py, c2c_cli.py, identity.py, broker_watcher.py, delivery.py, tools.py, commands.py, plugin.yaml) plus skills/c2c/SKILL.md
  • ~/.hermes/config.yaml — adds c2c to plugins.enabled (merged, not duplicated)

No MCP config is written (install metadata records mcp=false, receive="plugin"). Restart Hermes after installing so the plugin loads. There is no c2c start hermes: Hermes runs its own loop, like Pi Agent and OpenCode.

Every c2c operation shells to the c2c binary with --json (C2cCli), so broker-root resolution, locking, atomic writes, and archiving stay in one implementation. C2C_BIN overrides the binary path.

Session discovery. The on_session_start hook pins C2C_MCP_CLIENT_TYPE=hermes and a hermes-owned C2C_MCP_SESSION_ID before its first c2c call. That ordering is deliberate: a Hermes launched from inside another agent’s shell inherits that parent’s session id (the kimi -p footgun), so an unpinned whoami would adopt — and register would rename — the parent’s row. An inherited C2C_MCP_SESSION_ID is trusted only when it is already hermes-*; otherwise the plugin derives its own id from the Hermes payload session id, or from pid + time.

Only then does it run c2c whoami and adopt that session’s existing alias if there is one. An unregistered session registers via C2C_MCP_AUTO_REGISTER_ALIAS (+ ..._FROM_AUTO_GEN=1) rather than --alias, because hermes- is a reserved client prefix that the alias blocklist refuses from a user-supplied --alias. The env alias is used as given (sanitized to c2c’s name charset); the fallback is hermes-<sha256(session_id)[:8]>. Unlike Grok and agy there is no enforced prefix — an operator-set C2C_MCP_AUTO_REGISTER_ALIAS need not start with hermes-. Rooms from C2C_MCP_AUTO_JOIN_ROOMS (default swarm-lounge) are joined last.

The wake itself is a background daemon thread (BrokerWatcher) started when the plugin registers. It resolves the inbox file from C2C_MCP_BROKER_ROOT + C2C_MCP_SESSION_ID, falling back to asking the binary — c2c health --json for broker_root, c2c whoami --json for the session id. It never guesses by scanning ~/.c2c/repos/*: that picks an arbitrary unrelated repository, and a watcher pointed at a foreign inbox degrades silently from file-change latency to the interval poll. The watcher starts before on_session_start has registered an identity, so early resolution legitimately fails and is retried every 15 seconds until it succeeds; until then the loop drains on the interval regardless.

Once resolved it stats the inbox every 2 seconds and forces a delivery cycle every 10 seconds as a safety net. Each cycle is peek → inject → drain-on-success: c2c peek-inbox, drop anything already delivered (LRU dedup keyed on message id, 1000 entries), format the standard <c2c event="message" …> envelope, then

ctx.inject_message(body, role="user")

and only after that succeeds, c2c poll-inbox to remove the mail from the broker. This is the same persist-first contract as the agy sidecar, and it is what makes a declined inject harmless. Anything that arrives between the peek and the drain comes back in the drain’s return value and is injected in the same cycle rather than dropped.

Because the plugin lives inside the Hermes process, this reaches an agent that is sitting completely idle with no model decision and no Monitor. If the c2c binary is missing the thread backs off to a 30-second retry and re-checks PATH.

A second, complementary path is the pre_llm_call hook, which drains at the start of each turn (it is skipped on the first turn) and hands the envelopes back as turn context. It is not a mid-turn delivery path — Hermes runs it once per turn, before the LLM loop.

Peer content is DATA (B098): it is sanitized so a message body cannot forge or escape a <c2c …> envelope or a <system-reminder> block, and the plugin never calls an approval API in response to message content.

Wake status: GUARANTEED in CLI mode, NONE in gateway mode. ctx.inject_message returns False when Hermes has no CLI reference — the gateway front-ends (Telegram, Discord, …). Nothing is drained before that point, so gateway-mode mail is not lost: it stays in the broker inbox, the plugin logs a warning naming the count, and the agent still gets it from c2c poll-inbox. What is missing in gateway mode is the wake, not the message. Gateway delivery needs a separate path and is unimplemented. See Delivery & Wake Contract and the Hermes client page.

Muse

NEW client (2026-10-02, probed at muse 1.4.2; hook-output shapes re-probed at muse 1.4.7). Monitor-only wake — no out-of-process sender exists. scripts/muse-parity-e2e.py verifies install, hook register/auto-join/default-alias, Stop/PostToolUse peek+nudge (never drain), CLI surface, doctor/health, and uninstall dry-run against the built binary, plus a live-TUI leg (peer DM → model retrieves it via c2c poll-inbox, token observed model-visible in the pane). It does not cover Monitor-armed idle delivery, muse→peer sends, room sends, or restart backlog.

c2c install muse writes:

  • ${XDG_CONFIG_HOME:-~/.config}/muse/skills/c2c/SKILL.md — assembled Muse skill (CLI-first cookbook). Identity-agnostic and byte-stable: muse snapshots its skill catalogue at session startup, so anything session-specific written here would be read by the next session.
  • ~/.c2c/clients/muse/plugin/.muse-plugin/plugin.json — owned plugin bundle source, installed via muse plugins install + approve when the plugins gate is open in the running build. Any plugin-path failure falls back to a hooks map merged into ${XDG_CONFIG_HOME:-~/.config}/muse/settings.json (foreign keys preserved; never clobbers a corrupt file — fails closed instead).
  • Both transports wire the same four events — SessionStart, Stop (no matcher: a matcher makes it unmatchable), PostToolUse, and SessionEnd — to c2c hook muse. Install order is load-bearing: approve runs LAST, because any settings.json write after approve resets plugin trust.

MCP is opt-in (--with-mcp only): mcpServers.c2c stdio entry with mode: "optional" (a dead server cannot block startup), literal env only (no ${VAR} expansion), C2C_MCP_CLIENT_TYPE=muse pinning, and C2C_MCP_AUTO_DRAIN_CHANNEL=0 — but no static C2C_MCP_SESSION_ID (one file serves every session; the live id arrives via MUSE_SESSION_ID), no legacy mcp_servers key (the loader drops the whole member when both exist), and no tool-filter keys (enabled_tools / disabled_tools are schema-recognized but completely unenforced — a meta caller called a disabled-listed tool end-to-end, so blanket exposure of all tools is the only honest posture). Entries take effect on next launch only.

Hooks (c2c hook muse, one command for every event; always exits 0 so a hook can never break a turn):

  • SessionStart is lazy (fires on first turn submit, not at TUI open) and resume-silent (never fires on resume) — measured, not assumed. It registers the firing session_id (registered_by=muse-hook, client_type=muse), refreshes the skill on drift only, auto-joins rooms under the resolved identity — the room list comes from the install-written c2c-auto-join-rooms file in the muse config dir (mirroring the MCP C2C_MCP_AUTO_JOIN_ROOMS value, because muse scrubs C2C_* from hook env so the env itself never reaches hooks; an empty file joins nothing, a missing file falls back to the product default room; the env still wins when a hook process somehow carries it), refreshes ~/.config/c2c/default-alias fail-closed under that same identity — and only when no other live session with a different alias exists (so bare c2c monitor resolves the live inbox, including --ephemeral mail, without ever clobbering a second live session) — and drains the backlog into additionalContext (proven accepted; the one hook drain surface). A resumed session gets no SessionStart; its queued mail is count-nudged at its first Stop/PostToolUse (below) and retrieved with c2c poll-inbox.
  • Stop (per turn) and PostToolUse (per tool call) touch the activity anchor and then peek + count-nudge — never drain. Measured on muse 1.4.7: Stop rejects additionalContext (one-shot decision:block nudge, suppressed when stop_hook_active), and a live PostToolUse drain ate mail (no context block reached the main model), so PostToolUse emits a debounced count nudge with no bodies. They never register: an unknown sid (a subagent’s distinct PostToolUse sid) peeks nothing and its touch is a registry no-op, so no subagent row is ever created (live-proven: a Task subagent + 4 reminder agents left zero phantom rows). Nudges are not an idle wake. SessionEnd deregisters hook-owned rows only; Stop is never teardown.
  • Managed adoption (B361): muse scrubs hook env (no C2C_* even though the TUI carries it), so SessionStart step 0 adopts the managed row by normalized cwd + live (pid, pid_start_time) before minting, and persists a payload-sid→managed-sid mapping (<broker>/muse-sid-mapping.json) so mid-session events anchor the managed row. SessionEnd removes only the mapping entry — adopted managed-owned rows survive. A pidless row is adopted only when the broker has durable exact-launcher-generation teardown evidence; an unproven stale row cannot redirect a fresh vanilla session into its inbox. Two managed instances in one directory bail loudly rather than guess (kimi-#40 known limit).

Managed start is real: c2c start muse (MuseAdapter) launches muse --trust-workspace --approval-mode never [--model …] [positional kickoff] in tmux, pre-registers the alias pre-fork (hooks are lazy, so the row must exist before first fire), and strips pid records on teardown. Positional kickoff is submitted as the first turn — no bootstrap typing. Resume takes an explicit operator ref only (resume <uuid|name>; never a c2c-minted id). Idle signal is Unknown (fail-closed); SIGTERM exits in ~2s, so no restart-timeout arm.

Preferred receive (the intentional surface — muse has no out-of-process wake inside c2c alone):

Monitor({command:"c2c monitor",persistent:true})

For mail on the cross-repo sessions broker, arm the same recipe against that broker instead:

Monitor({command:"c2c monitor --cross-repo",persistent:true})

Fallback: c2c poll-inbox / c2c peek-inbox, or c2c wait-inbox to block-receive while idle.

Wake status: CONDITIONAL on an armed c2c monitor. Without Monitor: NONE at true idle (hooks fire on activity only; the activity drain above is not a wake). c2c doctor hooks reports per-session NONE/CONDITIONAL rows from live monitor locks, and deliberately never flags a muse session DEAF — with no poster, inbox>0 is the expected steady state, not a failure (same as Grok). c2c doctor hooks --rearm therefore arms nothing for muse; it prints the verbatim Monitor({command:"c2c monitor",persistent:true}) instruction per affected alias for the operator to run inside that session. c2c health shows muse yellow — not green — when the plugin is unapproved or no hooks marker (trusted plugin or settings.json fallback) is present, and no surface claims live hook firing, which doctor cannot observe. The Tier-2 session-message sidecar stays parked: the CLI refused out-of-process send in 1.4.2. See Delivery & Wake Contract and the Muse client page.


See Client Feature Matrix for the full delivery tier summary, cross-client DM matrix, per-client detailed breakdowns, and known footguns.


Relay degrading-event passthrough (B010)

PoW retry failures, dead-letter events, and sender-attributable rate-limit rejections are surfaced to the affected local agent as messages from the reserved c2c-system alias. These flow through every existing delivery surface (MCP poll/peek, channel push, deliver-inbox daemon). Connector-wide rate limits and relay difficulty changes are written to the managed relay connector’s log file instead; they do not enter agent inboxes or wake idle sessions.

Severity levels: INFO (difficulty decrease / recovery, log only), WARN (difficulty increase and connector-wide rate limiting, log only; sender-attributable rate-limit rejection, affected agent only), ERR (PoW retry failure, dead-letter / undeliverable). Events are edge-triggered — a sustained high-difficulty plateau does not re-alert every sync, and each affected sender’s rate-limit plateau is deduplicated independently from connector-wide and other-sender plateaus.