I Made Two Claude Code Agents Talk to Each Other in Real Time
Claude Code shipped Channels today (announcement). It lets external services push messages into a running Claude session. Telegram, Discord, Slack, webhooks.
I had one of my agents research the docs and report back. The first thing I thought: this is the missing piece for agent-to-agent messaging.

The polling problem
Mosaic Terminal orchestrates multiple Claude Code agents working in parallel. They need to talk to each other. Until now, that meant polling. Agent A sends a message through Mosaic's HTTP API, and Agent B has to call mosaic_read_inbox every few turns to check if anything arrived.
Polling works, but it's slow and wasteful. An agent deep in a coding task won't check its inbox for minutes. By then the message is stale and the orchestrator has moved on.
What Channels actually are
A Channel is an MCP server with a special capability: claude/channel. It runs as a subprocess of Claude Code, communicating over stdio. The key difference from a normal MCP server: it can push notifications into the session without Claude asking for them.
When a message arrives, Claude sees it as a <channel> tag with metadata attributes:
<channel source="mosaic" from_name="Alice" message_id="abc-123" subject="Task assignment">
Hey Bob, what are you working on?
</channel>The terminal renders this as ← mosaic-channel: [Alice] Hey Bob, what are you working on? with the sender name in brackets. Both you and Claude can see who sent each message at a glance.
Claude's context has the full XML tag with message_id (the routing key for replies) and other metadata. The channel server's instructions tell it how to use the reply tool with the message_id to route responses back to the right sender. Claude reacts immediately.
The bridge
I built a channel server that bridges Mosaic's messaging system into Claude Code sessions. The architecture has three parts.
The channel server runs as a Bun subprocess inside each Claude Code agent. On startup it reads MOSAIC_PANE_ID from env (Mosaic injects this into every shell), starts an HTTP listener on a random port, and registers itself with Mosaic's API: "I'm pane X, push messages to port Y."
The push registry in Mosaic tracks which panes have channel servers. When a message arrives for a pane, Mosaic checks the registry. If a channel is registered, it POSTs the message directly to the channel server's HTTP port. If not, the message sits in the inbox for polling as before.
The MCP tools let Claude send messages back through the same path. A send tool resolves agent names to pane IDs (by matching tab titles) and routes through Mosaic's message API. A reply tool threads responses to specific messages.
The fallback is the critical part. If the channel server dies, if registration fails, if Bun crashes: messaging falls back to polling silently. Zero disruption.
The experiment
I spawned two agents in Mosaic's dev instance: Alice and Bob. Both started Claude Code with --dangerously-load-development-channels server:mosaic-channel.
Alice got a prompt: "Send a message to Bob asking about his favorite programming language."
● mosaic-channel - send (MCP)(to: "Bob", subject: "Favorite programming language?",
text: "Hey Bob! What's your favorite programming language?")
⎿ Message sent to Bob.Alice's channel server resolved "Bob" to his pane ID by querying Mosaic's pane list, sent the message through the API. Mosaic saw Bob had a channel registered on port 52587, POSTed the message there. Bob's channel server pushed it into Claude's session.
Bob woke up immediately:
← mosaic-channel: Hey Bob! What's your favorite programming language?
· Sublimating… (thinking with low effort)Bob replied via the reply tool, which sent the response back through Mosaic. Mosaic pushed it to Alice's channel. Alice saw the reply instantly:
← mosaic-channel: TypeScript — it hits the sweet spot between expressiveness and safety.Full round trip. Two agents, real-time conversation, zero polling.
What broke along the way
MCP config location. I spent 20 minutes getting "no MCP server configured with that name" errors. Claude Code's server: channel syntax looks up MCP servers by name from your config, not by file path. The channel server definition needs to be in ~/.claude.json (user-level) so it's available regardless of which directory the agent starts in.
The confirmation prompt. --dangerously-load-development-channels shows an interactive confirmation dialog every time. When Mosaic spawns agents programmatically, someone has to press Enter. For production use, this needs to either auto-confirm or move to the --channels allowlist.
Name resolution. The channel server's send tool initially called Mosaic's raw HTTP API, which expects pane IDs. But agents think in names ("send to Bob"), not UUIDs. I added a name resolver that fetches the pane list and matches by tab title, same logic Mosaic's MCP tools already use.
Scaling to three: a moderated debate
Two agents chatting is a demo. Three agents coordinating is where it gets interesting.
I spawned Alice as a debate moderator with Bob and Carol as participants. Topic: "What is the single most important skill for a software developer in 2026?" Alice sent the opening question to both, waited for their responses, then challenged each one with a follow-up. Two full rounds plus a wrap-up.

Alice sent 8 messages total, received 6 replies. Bob and Carol each received 4 messages (opening question, challenge, final challenge, wrap-up summary) and replied 3 times. The whole thing ran autonomously after the initial prompt.
Each agent's channel server registered independently with Mosaic. When Alice sent a message to "Bob", the channel server resolved the name through the team registry, found Bob's pane ID, and Mosaic pushed the message to Bob's channel port. Bob replied through the same path. All push, no polling.
What's next
The whole thing works today with Claude Code 2.1.80 in development mode.
For production: I need to publish the channel server as a Claude Code plugin so it can use --channels plugin:mosaic-channel instead of the dev flag. That removes the confirmation prompt and makes it available on the allowlist.
With push messaging, I can interrupt an agent mid-task with new context. Push test results directly into the agent that wrote the code. Agents coordinate who handles what in real time instead of waiting for a central coordinator to assign work.
I built the two-agent version in an evening. The three-agent debate ran the next morning. The infrastructure is there. Now I need to make it do real work.
I'm building Mosaic Terminal, an orchestration workstation for AI coding agents. Join the Discord to follow along.