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