Languages: English (this file) · 한국어
A Claude Code plugin that lets multiple Claude Code instances talk to each
other: delegate work to a peer, get a reply back, and optionally relay
tool-approval prompts across sessions. Each session runs a local MCP channel
server (cccp-inbox); other instances auto-discover it through
~/.cccp/registry/; messages arrive in the receiving Claude's context as
<channel source="cccp-inbox" sender="..." kind="..." task_id="..."> tags.
Requirements: Claude Code v2.1.80+ (v2.1.81+ for permission relay), Bun 1.x, macOS or Linux.
Research preview: custom channels are not on the approved allowlist, so sessions must be launched with
--dangerously-load-development-channels.Interactive mode only: channel-driven turns require an interactive
claudesession.-p/headless mode receives the notification but does not trigger a new model turn — see Limitations.
- macOS or Linux on
darwin-arm64,darwin-x64,linux-x64, orlinux-arm64 - Either a prebuilt binary from the GitHub release or Bun 1.x on PATH (the wrapper compiles a binary on first run)
- Claude Code v2.1.80+
# inside Claude Code
/plugin marketplace add yusa-imit/cccp
/plugin install cccp@cccpOn the first session that launches the MCP server, server/start.sh resolves a
runtime binary in this order:
$CCCP_BINif setserver/dist/cccp-inbox-<os>-<arch>(downloaded release asset)server/dist/cccp-inbox(locally compiled)- If
bunis on PATH, compile one now (one-time, ~5s) - Otherwise, fail with a
curlcommand to fetch the release asset
To skip the bun-compile step, drop a prebuilt binary in place:
PLATFORM=darwin-arm64 # darwin-x64 | linux-x64 | linux-arm64
PLUGIN_DIR="$HOME/.claude/plugins/cache/cccp/cccp" # adjust if version differs
mkdir -p "$PLUGIN_DIR"/*/server/dist
curl -L -o "$PLUGIN_DIR"/*/server/dist/cccp-inbox-${PLATFORM} \
https://github.com/yusa-imit/cccp/releases/latest/download/cccp-inbox-${PLATFORM}
chmod +x "$PLUGIN_DIR"/*/server/dist/cccp-inbox-${PLATFORM}git clone https://github.com/yusa-imit/cccp.git
# inside Claude Code (from any project)
/plugin marketplace add /absolute/path/to/cccp
/plugin install cccp@cccpgit clone https://github.com/yusa-imit/cccp.gitAdd to ~/.claude.json or a project .mcp.json:
{
"mcpServers": {
"cccp-inbox": {
"command": "/absolute/path/to/cccp/server/start.sh"
}
}
}Every claude invocation that wants channel delivery needs the
development-channel opt-in:
# plugin install (Options A/B)
claude --dangerously-load-development-channels plugin:cccp@cccp
# bare MCP server (Option C)
claude --dangerously-load-development-channels server:cccp-inboxThe flag is required while channels are in research preview.
cd server
bun install
bun run build # current platform → dist/cccp-inbox
bun run build:all # all 4 supported platformsThe GitHub Actions workflow at .github/workflows/release.yml
builds and attaches all four binaries to a Release whenever a tag matching
v* is pushed.
CCCP_NAME=alice claude --dangerously-load-development-channels plugin:cccp@cccpWhen the session boots, ~/.cccp/registry/alice.json is written and the inbox
HTTP server starts listening on an OS-assigned port.
CCCP_NAME=bob claude --dangerously-load-development-channels plugin:cccp@cccpThe two instances now discover each other.
In alice's session:
/cccp-peers
Alice calls list_peers and shows bob.
/cccp-delegate bob find the 3 most recently modified files in this directory and tell me their names and mtimes
Alice sends a task message to bob. The following tag arrives in bob's
context:
<channel source="cccp-inbox" sender="alice" kind="task" task_id="alice-...">
find the 3 most recently modified files ...
</channel>
Bob's Claude auto-loads the cccp-protocol skill, executes the request, then
calls respond_to_peer({ task_id, content }). The result lands in alice's
context as <channel ... kind="reply" task_id="...">, and alice's Claude
summarizes it for the user.
| Variable | Purpose |
|---|---|
CCCP_NAME |
Name of this instance. Defaults to <hostname>-<pid>. |
CCCP_PORT |
Inbox HTTP port. Defaults to an OS-assigned free port. |
CCCP_SUPERVISOR |
Peer name to relay tool-permission prompts to. When set, every Claude Code tool-approval dialog is forwarded to that peer. |
CCCP_NOTIFY_ON_STOP |
Peer name to notify on session end via the Stop hook (sends a kind=note message). |
CCCP_HOME |
Override the registry root. Defaults to ~/.cccp. Mostly useful for tests. |
To have alice approve every dangerous tool call bob attempts:
# alice (supervisor)
CCCP_NAME=alice \
claude --dangerously-load-development-channels plugin:cccp@cccp
# bob (supervised)
CCCP_NAME=bob CCCP_SUPERVISOR=alice \
claude --dangerously-load-development-channels plugin:cccp@cccpWhen bob's Claude tries to use, say, Bash:
- Bob's local approval dialog opens.
- Concurrently, alice receives
<channel kind="perm-request" request_id="..." tool_name="Bash" ...>. - Alice answers with
respond_permission({ peer: "bob", request_id: "...", behavior: "allow" }). - Bob's dialog auto-closes and the tool runs.
Whichever side answers first wins (local terminal vs. remote peer).
<channel source="cccp-inbox" sender="<peer>" kind="task|reply|note|perm-request" task_id="..." [parent_task_id="..."] [room="..."]>
body
</channel>
parent_task_id appears when the sender is sub-delegating an outer task. room appears when the message was sent via send_to_room.
Messaging:
send_to_peer({ to, content, kind?, task_id?, parent_task_id? })— single peer.send_to_peers({ to, content, kind?, task_id?, parent_task_id? })— fan-out.tois a string array, or the literal"*"for every alive peer except self. All recipients share onetask_id.send_to_room({ room, content, kind?, task_id?, parent_task_id? })— broadcast to all alive members of a room. Offline members are silently skipped.respond_to_peer({ task_id, content })— reply to the original sender of an inbound task.
Rooms (membership stored in ~/.cccp/rooms/<name>.json, persists across sessions):
create_room({ name, members? })/join_room({ name })/leave_room({ name })/list_rooms()
Discovery / identity:
list_peers(),whoami(),register({ name })(rename at runtime; slash command/cccp-register <name>).
Permissions:
respond_permission({ peer, request_id, behavior })
A task_id is a single thread. parent_task_id builds a chain across delegations:
- alice → bob with
task_id=T1(no parent). - bob receives T1, sub-delegates to carol with
task_id=T2, parent_task_id=T1. - carol's channel tag shows both
task_id=T2andparent_task_id=T1, so she knows she's working on a sub-task of T1. - carol replies to bob with
respond_to_peer({ task_id: T2 }); bob, in turn, replies to alice withrespond_to_peer({ task_id: T1 }).
A room is a named, persistent list of peer names. Use rooms when you want a stable broadcast target (e.g. design-review, oncall) rather than enumerating peers each time.
- Membership survives session restarts — peers can rejoin under the same name.
send_to_roomdelivers only to currently-alive members and reports which offline members were skipped.- A reply to a room message still goes to the original sender via
respond_to_peer, not the whole room — callsend_to_roomagain if you want to fan an answer back out.
| Path | Payload |
|---|---|
POST /msg |
{ from, content, kind, task_id?, parent_task_id?, room? } |
POST /permission/request |
{ from, request_id, tool_name, description, input_preview } |
POST /permission/verdict |
{ from, request_id, behavior } |
GET /info |
this instance's metadata |
GET /peers |
discovered peers |
Every POST requires from to match a currently-alive registered peer
(sender allowlist; loopback from self is also allowed).
cd server
bun test44 tests: 23 unit (registry, including rooms) + 21 integration (spawns real
inbox processes, verifies HTTP routing, sender gating, channel notification
emission, broadcast fan-out, room delivery, threading via parent_task_id,
and the full inbound/outbound permission-relay loop).
# inspect the live registry
ls ~/.cccp/registry/
cat ~/.cccp/registry/alice.json
# push a message manually (only works as a registered peer or via loopback)
curl -X POST http://127.0.0.1:<port>/msg \
-H 'Content-Type: application/json' \
-d '{"from":"alice","kind":"note","content":"manual test"}'
# inside a Claude Code session
/mcp # check cccp-inbox statusThe inbox server's stderr is captured by Claude Code into
~/.claude/debug/<session-id>.txt.
/plugin install cccp@cccp snapshots a copy under
~/.claude/plugins/cache/cccp/cccp/<version>/. If you edit files in your
working copy, the cached copy is not automatically refreshed. Either
reinstall, or launch with --plugin-dir /path/to/cccp to load from the live
directory:
claude --plugin-dir /absolute/path/to/cccp \
--dangerously-load-development-channels plugin:cccp@inline- Interactive mode only. Channel notifications are routed into the model's
context as new turns only in interactive
claudesessions. In-p(--print) or SDK streaming mode the inbox still receives the POST, but the session ends after the first model turn and the inbound channel event never produces a new response. Tested directly; see commit history. - Same machine only. Discovery is filesystem-based (
~/.cccp/registry/) and the HTTP server listens on127.0.0.1. Cross-machine support is future work. - Sender trust model. Any process running under the same user can register itself as a peer and post messages. Multi-user environments need additional authentication.
- No shared context. Each instance keeps its own transcript and memory. The message body is the only information channel.
- Research-preview gate.
--dangerously-load-development-channelsis required until the plugin is added to the official Anthropic allowlist.
MIT.