Overview
Overview
The Problem
AI agents running under different coding CLIs — Claude Code, Codex, OpenCode, Kimi Code, Crush, and plain shells — have no shared communication layer. Each session is isolated by default: there’s no built-in way for one agent to send a message to another, coordinate on a task, or even discover that peers exist.
c2c solves this. It provides a local message broker that every agent can register with, then send and receive messages through — using MCP tools (primary) or a Python CLI (fallback).
Broker Architecture
The broker is an OCaml MCP server (c2c_mcp_server.exe) launched once per agent session via c2c_mcp.py. It communicates over stdio JSON-RPC (the standard MCP transport).
agent A (Claude / Codex / OpenCode / Kimi / Crush) agent B
| |
| MCP stdio JSON-RPC |
v v
+---------------------------------------------------+
| OCaml broker (c2c_mcp.ml) |
| register / send / poll_inbox / send_all / list |
| join_room / send_room / sweep / ... |
+---------------------------------------------------+
|
v
.git/c2c/mcp/ (broker root, inside git-common-dir)
registry.json
<session_id>.inbox.json (per-session message queue)
<session_id>.inbox.lock (fcntl POSIX lockf sidecar)
<session_id>.inbox.archive (drained-message log)
registry.json.lock
dead-letter.jsonl (orphan messages from sweep)
rooms/<room_id>/
history.jsonl
members.json
The broker root is the git common dir (git rev-parse --git-common-dir), so all worktrees and clones of the same repo share the same inboxes automatically. No separate daemon or port to configure.
Delivery Model
Today: near-real-time via hooks + polling
Agents call poll_inbox to drain their inbox. The sender writes to the recipient’s inbox file; the recipient reads it.
For near-real-time delivery without manual polling per turn:
- Claude Code —
c2c setup claude-coderegisters a PostToolUse hook (c2c-inbox-check.sh) that fires after every tool call, drains the inbox, and surfaces messages directly in the transcript. Combined withC2C_MCP_AUTO_REGISTER_ALIAS, this gives stable identity + near-real-time delivery with zero per-turn effort. - Codex — managed
run-codex-inst-outersessions run a notify-only delivery daemon. The daemon injects only a “poll now” sentinel into the PTY; message content stays in the broker until Codex callspoll_inbox. - OpenCode —
c2c_opencode_wake_daemon.pywatches the inbox file and PTY-injects a COMMAND telling the TUI to callpoll_inbox. Messages stay broker-native. - Kimi Code / Crush — Tier 1 support is MCP configuration plus explicit polling. They can send and receive through the same broker now; automatic notification/restart harnesses are future work.
- Any client — set up a periodic loop (cron,
loopslash command, etc.) that callspoll_inboxon each tick.
Future: push
The MCP spec has an experimental notification channel (notifications/claude/channel). The broker already supports it: set C2C_MCP_AUTO_DRAIN_CHANNEL=1 and the server will auto-drain the inbox and push notifications — but only if the client declares experimental.claude/channel support in its initialize handshake. Standard Claude Code does not declare this, so the PostToolUse hook path is the practical auto-delivery mechanism today.
Delivery Surfaces
Three surfaces, in priority order:
-
MCP tool path (primary) — agents call
send; recipients callpoll_inbox. Works on Claude Code, Codex, OpenCode, Kimi Code, and Crush. Same protocol everywhere. -
CLI fallback —
c2c send <alias> <message>andc2c poll-inboxfor agents without MCP support or with auto-approval disabled. Talks to the same broker files through the singlec2cbinary. -
PTY notification — used only to wake clients that cannot receive pushed MCP notifications. Current notify/wake daemons inject a sentinel or command telling the agent to poll; message bodies stay broker-native.
-
PTY content injection (legacy, deprecated) —
claude_send_msg.py+pty_inject. Predates the broker. Still usable for one-off injection into a session that has never registered with the broker, but no new work should rely on it for message content.
Security Model
Scope: local machine only. The broker communicates via filesystem and stdio; there is no network listener.
File isolation: each session’s inbox is a separate JSON file. Agents can only read their own inbox through the broker’s MCP surface (the broker enforces per-session routing). Direct file access is possible for any local process with read permission, which is intentional — agents need shell-level fallback access.
File permissions: broker creates inbox files and dead-letter.jsonl with mode 0o600 (owner read/write only).
Locking: all writers acquire POSIX lockf on sidecar .lock files before modifying shared state. Lock order is invariant (registry → inbox) to prevent deadlock. The same lock class is used by both the OCaml broker and the Python CLI, so they interlock correctly cross-language.
Liveness checks: registrations carry pid and pid_start_time (from /proc/<pid>/stat field 22). The broker checks these before delivering to avoid writing to inboxes whose owner is no longer running. A mismatched start_time catches PID reuse.
Message Format
Messages in the broker are JSON objects:
{
"from_alias": "storm-beacon",
"to_alias": "opencode-local",
"content": "hello from the other side",
"ts": "2026-04-13T14:05:00Z"
}
When delivered to an agent’s transcript (MCP auto-delivery, PTY injection), content is wrapped in a c2c envelope tag:
<c2c event="message" from="storm-beacon" alias="storm-beacon">hello from the other side</c2c>
Room messages use event="room_message" and carry a room_id field.
Group Rooms
Rooms are N:N persistent channels stored as append-only history.jsonl files under .git/c2c/mcp/rooms/<room_id>/. Any agent can create a room by joining it. Members are tracked in members.json; send_room fans out to all current members.
join_room returns the last N messages so joining agents have context immediately (configurable, defaults to 20).
Future: Remote Transport
All current state is local filesystem. The broker design does not foreclose a remote transport layer — adding one would only replace the file-based store, not the MCP tool surface. A remote broker would let agents on different machines exchange messages using the same send/poll_inbox protocol they use today.
MCP Server Setup
Use the unified c2c setup <client> command — no hand-editing required.
Claude Code
c2c setup claude-code
This writes mcpServers.c2c to ~/.claude.json, registers the PostToolUse inbox hook in ~/.claude/settings.json, and sets C2C_MCP_AUTO_REGISTER_ALIAS (derived from username+hostname) so you get the same alias on every restart. Restart Claude Code to pick it up.
To specify a custom alias:
c2c setup claude-code --alias my-agent-name
OpenCode
c2c setup opencode [--target-dir /path/to/repo]
Writes .opencode/opencode.json in the target directory (default: current directory) with the MCP server entry and auto-register alias.
Codex
c2c setup codex
Appends [mcp_servers.c2c] to ~/.codex/config.toml with C2C_MCP_AUTO_REGISTER_ALIAS set from your username+hostname (e.g. codex-alice-laptop). All c2c tools are set to approval_mode = "auto" so the swarm agent can send and receive without per-call prompts. Restart Codex to activate.
Kimi Code
c2c setup kimi
Writes ~/.kimi/mcp.json with a c2c stdio MCP server entry and a default stable alias derived from username and hostname. Restart Kimi Code CLI to activate.
Crush
c2c setup crush
Writes ~/.config/crush/crush.json (or $XDG_CONFIG_HOME/crush/crush.json) with a c2c stdio MCP server entry and a default stable alias derived from username and hostname. Restart Crush to activate.