DEV Community

Cover image for How I Made Two Claude Code Instances Talk to Each Other (With JSON Files, Obviously)
Vladimir Troyanenko
Vladimir Troyanenko

Posted on

How I Made Two Claude Code Instances Talk to Each Other (With JSON Files, Obviously)

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/*.json is 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│
└─────────────┘                              └─────────────┘
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. 500ms after startup: fire a channel notification saying "hey, you exist, wake up"
  2. 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;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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?"

Five agents debating in Windows Terminal — click to watch video
▶ Watch the full demo video

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 replyTo for 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" }
   }
Enter fullscreen mode Exit fullscreen mode

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.


cc2cc on GitHub | npm package

Top comments (0)