Per-Client Delivery
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); and agy (Google Antigravity) uses agentapi inject via the deliver-watch sidecar (see Antigravity (agy)). - 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 full-body receive: for long-running Claude sessions, run a persistent Monitor with the canonical recipe:
Monitor({ description: "c2c inbox watcher", command: "c2c monitor", 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). 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 codexuse the app-server transport by default on a supported Codex (codex-cli ≥ 0.144) — there is no flag to set. Its delivery stack is wired into managed supervision (B131): the supervisor injects inbound c2c mail into the thread’s model-visible history on arrival (draft-safe), and starts one gated model turn for eligible local mail when the thread is idle and DND is off (idle auto-turn is immediate; inject / auto-turn failures force-retry or re-batch after ~2 minutes — B168). Older Codex or an app-server startup failure falls back automatically to the hook boundary:c2c install codexinstallsUserPromptSubmit,PostToolUse,SessionStart, andSessionEndhooks runningc2c hook codex, which auto-registers the session, drains inbound broker messages, and returns them throughhookSpecificOutput.additionalContext— hook delivery happens when a hook fires, not on arrival. Vanilla (non-managed) Codex sessions use the 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 writes ac2c-sessionidentity skill 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 write ac2c-sessionidentity skill — 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 + conversation from the CLI log (and can mint a wake conversation) when hooks never wrote it. Hooks alone do not wake an idle TUI. Managed viac2c start agy. 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. After a successful app-server start the launcher binds that banner alias into the broker before interaction (B172), so first-turnwhoami/ send match the banner without a re-init. Hooks inherit the launcher session id viaC2C_CODEX_APPSERVER_SESSION(B166/B137) and do not mint a second identity. --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
reaps the app-server when the frontend exits (no orphan processes), 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 next start/resume. 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)
Managed c2c start codex / c2c new codex launch codex app-server plus the
stock remote TUI attached to it over an authenticated loopback boundary. This is
the default and only managed path for a supported Codex (codex-cli ≥ 0.144) —
there is no flag to enable it, and no user-facing way to select hooks. Older
Codex or a genuine app-server startup failure falls back automatically to the
hook-backed launch (a hidden C2C_CODEX_FORCE_HOOKS=1 escape exists for operator
testing only).
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:
- Authenticated local boundary (required). The app-server always listens
on loopback with
--ws-auth capability-tokenand a per-unit 256-bit capability token: unauthenticated same-UID 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. The raw token is passed to the frontend by environment variable name only (never argv, never disk, never logs). Any future TCP/WebSocket exposure beyond loopback requires the same bearer authentication plus an explicit exposure warning; do not forward the port without it. - 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); 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
registers the routable app-server alias (the one c2c instances reports and the
delivery loop drives) immediately after a successful app-server start (B172
— banner alias is broker-visible before first interaction; no re-init needed to
fix alias drift) and hands its session id to the stock Codex frontend’s hooks
via the inherited C2C_CODEX_APPSERVER_SESSION marker (exported before the
frontend is spawned — B166/B137). c2c hook codex adopts that identity instead
of self-registering a second alias, so c2c list shows exactly one entry per
session. The hook is then identity-only: it drains nothing (the delivery loop
owns arrival-time delivery of the repo inbox), so there is no double-drain.
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 (validated on 0.144.1). 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