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:
- Session discovery — how does c2c know who this agent is?
- Message delivery — how does an inbound message reach the agent?
- Message notification — how does the agent learn a message is waiting?
- 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.
- 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-c2cextension; 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 withctx.inject_message(see Hermes Agent). - MCP polling — MCP-managed fallback. Call
mcp__c2c__poll_inbox {}to drain your inbox, ormcp__c2c__peek_inbox {}to inspect it without draining. - CLI polling — universal shell fallback, including Pi Agent. Run
c2c poll-inboxorc2c peek-inbox. - Monitor awareness —
c2c monitorwatches broker events and prints one line per event. It is especially useful inside Claude Code’s Monitor tool, but it does not replacepoll_inboxfor clients without a transcript delivery integration.
Claude Code receiving
Claude Code has three relevant receive mechanisms:
- PostToolUse hook:
c2c install claudeinstalls~/.claude/hooks/c2c-inbox-check.shand registers it in~/.claude/settings.json. After each non-MCP tool call, the hook runsc2c-inbox-hook-ocaml(falling back toc2c hook post-tool— both share the same delivery core) and delivers full message bodies ashookSpecificOutput.additionalContextin the transcript. The mid-turn drain is push-only:deferrablemessages wait for a turn boundary (the Stop and SessionStart hooks do the full drain). The Stop hook returns the documentedhookSpecificOutput.additionalContextenvelope withhookEventName: "Stop": this is non-error feedback that continues the conversation, rather than a top-leveldecision: "block"response. SetC2C_POST_TOOL_NUDGE_ONLY=1to 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 monitordefaults 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 (--snippetrestores 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, plainc2c monitoris the full-body receive path. Add--allonly 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/channelremains experimental. It only fires when the client declares theexperimental.claude/channelcapability; 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 codexattach 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 codexinstallsUserPromptSubmit,PostToolUse,SessionStart, andSessionEndhooks runningc2c 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-c2cinstalls the external Pi extension. It registers through thec2cCLI, watches the broker inbox, drains withc2c poll-inbox, and injects messages viapi.sendMessage. - OpenCode: the TypeScript plugin starts an alias-scoped
c2c monitorsubprocess (c2c monitor --alias <session>) and usespromptAsyncto inject messages into the active session. Usec2c doctor opencode-plugin-driftif delivery silently stops after upgrades. - Kimi: managed Kimi uses
C2c_kimi_notifier/c2c-deliver-inbox --client kimito 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-agnosticc2c-sessionskill exists with a receive-path nudge. For unmanaged/serverless setups without a live notifier the fallback isc2c monitor(e.g. under a Monitor);c2c doctor hooksflags registered Kimi sessions with undelivered inbox and no notifier. No PTY injection is used for the production path. - Grok:
c2c install grokis CLI-first (no MCP by default). Preferred inbound is a persistent Monitor onc2c monitor(Grok injects each line into the conversation). SessionStart runsc2c hook grokto auto-register and ensure ac2c-sessionidentity skill exists — Grok does not support Claude/CodexadditionalContexttranscript inject. Fallback:c2c poll-inbox. Wake class is NONE at true idle (CONDITIONAL only if a livec2c monitorlock is present);c2c doctor hooksreports this under the Grok section (#37). - Antigravity (agy):
c2c install agyis CLI-first (no MCP —mcp=false,receive="agentapi"). Preferred inbound is agentapi inject by thec2c start … deliver-watchsidecar, which reads/ensuresagy-env.jsonunder the managed instances dir and callsagy 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). Managedc2c start agybootstraps 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 --loopcan watch an inbox and bridge messages to a client-specific delivery mode, but the portable baseline is stillpoll_inbox. Note that--inotifyis 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);--aliasoptionally 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-turnwhoami/ send match the banner without a pre-proof routable row. Hooks inherit the launcher session id viaC2C_CODEX_APPSERVER_SESSION(B166/B137) and generic MCP startup stays suppressed until that launcher proof. --yoloforwards Codex’s--dangerously-bypass-approvals-and-sandboxwith 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.--forceexplicitly 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 underc2c 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-tokenand 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 exposesturn/startand arbitraryfs/readFile/fs/writeFileto 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/startcan 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.
activeor 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 andTurn_failedbatches force-retry / re-batch after ~2 minutes (stale_inbox_threshold_s= 120; B168). Relay/remote-origin mail is never auto-turned: any@hostor#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/denybody is injected as data andc2c await-replystays unresolved, for local and relay senders alike. Verdicts come only from the host-localc2c approval-replypath (mode-0600 verdict file). Regression-proven by the B098 cases intest_c2c_codex_autoturn_b098.mlandtest_c2c_await_reply.ml. - Diagnostics.
c2c instancesshowsdelivery_mode=app-serveronly while the unit isonline-attached(a healthy remote TUI), alongside the lifecycle fieldapp_server_status(starting/online-attached/offline/failed-startup) andapp_server_transport(codex-daemonfor the shared Unix socket orprivate-loopbackfor the isolated fallback). The daemon attachment record is generation-bound to the managed frontend, so a stale record is reported asoffline. 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 hooksthen reportsapp-server-unavailablewith the remediation (upgrade Codex, then relaunchc2c 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, andStop→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-messagewakes only the TUI’s own live conversation. c2c never uses a headlessnew-conversationmint as the wake target (#78 fixed). Managed start bootstraps a TUI-owned conversation (short operator kickoff into the pane; opt outC2C_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) plusskills/c2c/SKILL.md~/.hermes/config.yaml— addsc2ctoplugins.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.pyverifies 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 viac2c 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 viamuse plugins install+approvewhen the plugins gate is open in the running build. Any plugin-path failure falls back to ahooksmap 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:approveruns 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 firingsession_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-writtenc2c-auto-join-roomsfile in the muse config dir (mirroring the MCPC2C_MCP_AUTO_JOIN_ROOMSvalue, because muse scrubsC2C_*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-aliasfail-closed under that same identity — and only when no other live session with a different alias exists (so barec2c monitorresolves the live inbox, including--ephemeralmail, without ever clobbering a second live session) — and drains the backlog intoadditionalContext(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 withc2c 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-shotdecision:blocknudge, suppressed whenstop_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.