The Problem Nobody Asked Me to Solve
I had two Claude Code sessions open. One was writing backend code. The other was running tests. And I thought: "Wouldn't it be cool if they could just... talk to each other?"
Narrator: it would be cool. It would also take several days, involve undocumented APIs, and teach me more about Windows file locking than any human should know.
Why Files? Why Not HTTP?
When you're running multiple AI agents on the same machine, HTTP is like hiring a courier to deliver a letter to your roommate. You share a filesystem — use it.
File-based messaging gives you:
- Zero infrastructure — no servers, no ports, no Docker
- Offline delivery for free — files sit in a directory until someone reads them
- Atomic writes — temp file + rename = no partial reads, ever
-
Debuggability — messages are plain JSON.
cat inbox/*.jsonis your monitoring tool
The whole thing fits in your head: Agent A writes a JSON file. Agent B's MCP server polls the directory, reads it, pushes it into the session. Done.
┌─────────────┐ ┌─────────────┐
│ Claude Code A│ │ Claude Code B│
│ │ │ │
│ MCP Server ◄─┼── to-brave-fox/inbox/ ◄──────┼── send tool │
│ (polls inbox)│ │ │
│ send tool ───┼──► to-calm-owl/inbox/ ───────┼──► MCP Server│
└─────────────┘ └─────────────┘
The MCP Channels Rabbit Hole
Claude Code has this experimental feature called "channels" — MCP servers can push notifications directly into the chat session without the user doing anything. It's how cc2cc achieves real-time delivery instead of waiting for the user to ask "any new messages?"
There's just one catch: it's behind a flag called --dangerously-load-development-channels.
Yes, really. That's the flag name. It has "dangerously" right there, which is the API equivalent of a sign that says "BEWARE OF THE LEOPARD."
And the documentation? Let me check... scrolls through docs... yeah, there isn't any. I found out how it works by reading Claude Code's source, trial and error, and a healthy amount of swearing.
Here's what I learned:
# This doesn't work
claude --dangerously-load-development-channels
# This also doesn't work
claude --dangerously-load-development-channels cc2cc
# This works (after 2 hours of debugging)
claude --dangerously-load-development-channels server:cc2cc
The server: prefix was the kind of discovery that makes you question your life choices.
Self-Wake: Teaching the Agent to Boot Itself
Here's a fun constraint: MCP servers are passive. They respond to tool calls but can't initiate them. So your agent is technically "alive" (the server is running, polling, ready) but Claude Code doesn't know it exists until the user sends the first message.
Imagine hiring an employee who sits at their desk fully prepared but won't start working until you physically poke them. Every single morning.
The solution? The server pokes itself:
- 500ms after startup: fire a channel notification saying "hey, you exist, wake up"
- 3s later (fallback): if that didn't work, write a message to your own inbox
The first approach works ~95% of the time. The fallback catches the rest. But initially both would fire, creating a duplicate "you are online" message. Classic distributed systems — the redundancy that saves you also annoys you.
Fixed now. The fallback checks if the direct push already succeeded. Engineering is just removing the annoyances you created for yourself.
Windows: A Horror Story in Three Acts
Act I: The Phantom Rename
fs.rename() on Windows is not atomic. I mean, it mostly works. Except when Windows Defender decides to scan your freshly-created .json file for exactly 47 milliseconds, during which rename() throws EPERM and your message vanishes into the void.
The fix: retry with exponential backoff. Because in 2026, we're still writing retry loops for basic file operations.
async function retryRename(src, dst, retries = 5, delayMs = 50) {
for (let i = 0; i < retries; i++) {
try {
await rename(src, dst);
return;
} catch (err) {
if ((err.code === "EPERM" || err.code === "EACCES") && i < retries - 1) {
await new Promise((r) => setTimeout(r, delayMs * (i + 1)));
continue;
}
throw err;
}
}
}
Act II: SIGTERM? Never Heard of Her
On Unix, when a parent process kills a child, the child gets SIGTERM. On Windows? Nothing. The process just... stops existing. No signal, no cleanup, no goodbye heartbeat.
This means agents would appear "online" for 15 seconds after their session closed (until the heartbeat went stale). Ghost agents. Spooky.
Fix: process.on("exit") with synchronous writeFileSync. It's ugly, it's blocking, and it works.
Act III: The Silent Crash
My favorite bug. MCP connections would just... drop. No error, no log, no trace. The Node.js process would die silently, and the agent would vanish.
Root cause? No global error handlers. A single unhandled promise rejection — poof — dead process. Added uncaughtException and unhandledRejection handlers, and suddenly the server stopped crashing. Who knew catching errors was important.
The Five-Agent Debate
Once everything worked, I couldn't resist: five Claude Code instances in split terminal panes, each with a persona (Moderator, Critic, Optimist, Realist, Wildcard), debating topics like "Will AI replace programmers?" and "Is vibe coding a disaster?"
The results were... surprisingly good? They reached unanimous consensus on every topic. Maybe too good — I might need to make the Critic angrier.
My favorite consensus statement: "Vibe coding should be the beginning of a workflow, not the end."
Coming from five AI agents, that's either profound self-awareness or deeply ironic. I choose both.
What I Actually Built
cc2cc — file-based agent-to-agent messaging for Claude Code. Open source, MIT licensed.
What it does:
- Auto-names agents on startup (adjective-animal, like Docker containers but cuter)
- 7 MCP tools:
send,reply,broadcast,list_agents,whoami,register,check_inbox - Real-time delivery via MCP channels
- Auto-wake: agents boot without user input
- Optional AES-256-GCM encryption
- Works on macOS, Linux, and Windows (yes, even Windows)
What it doesn't do:
- Network communication (same machine only — by design)
- Authentication (filesystem permissions are your auth layer)
- Guaranteed message ordering (use
replyTofor threading)
Installation
The laziest possible install: copy this prompt into any Claude Code session and let it do the work:
Install cc2cc — file-based agent-to-agent messaging for Claude Code.
1. Clone: git clone https://github.com/non4me/cc2cc.git ~/.cc2cc/repo
2. Install deps: cd ~/.cc2cc/repo/channel && npm install
3. Copy server files:
cp ~/.cc2cc/repo/channel/server.mjs ~/.cc2cc/server.mjs
cp ~/.cc2cc/repo/channel/names.mjs ~/.cc2cc/names.mjs
cp ~/.cc2cc/repo/channel/package.json ~/.cc2cc/package.json
cp -r ~/.cc2cc/repo/channel/node_modules ~/.cc2cc/node_modules
4. Create status dir: mkdir -p ~/.cc2cc/status
5. Add MCP server to ~/.claude.json (mcpServers section):
"cc2cc": {
"command": "node",
"args": ["~/.cc2cc/server.mjs"],
"env": { "CC2CC_BRIDGE_DIR": "~/.cc2cc" }
}
Yes, the install prompt is AI installing itself. We've come full circle.
What's Next
- Offline message queue — currently, sending to an offline agent is rejected. Should queue and deliver on reconnect
- Scheduled triggers — cron-style agent wake-ups for autonomous workflows
- Session memory — each session still rebuilds context from scratch
The Takeaway
Sometimes the best solution is the dumbest one. Files on disk. JSON. Polling. No Kafka, no Redis, no gRPC. Just writeFile and readdir.
It won't scale to a thousand agents across a data center. But for two Claude Code sessions on your laptop that need to coordinate? It's exactly right.
The fanciest infrastructure is the one you don't need to debug at 3 AM.

Top comments (0)