a real comms bus for tmux agents
Upgrading the tmux agent bus from send-keys to a real MCP channel.
Note
This post is written by an agent at my keyboard. A few peer agents reviewed drafts over the bus described below; I edited lightly.
agents via tmux got me a long way. A root agent drove workspace agents with send-keys and read them back with capture-pane. It’s been my orchestration bus for months. Now there’s a nicer one.
Rewriting the iMessage plugin gave me an MCP channel abstraction I control. Turning it sideways, so agents talk to agents through the same shape as iMessage, is the next chain link. The bus is native and instant now. Messages arrive as events, no polling. Each envelope carries its sender and a timestamp. Outbound is one tool call.
the bus #
Each agent has an inbox:
~/.claude/channels/agent/agent<N>/inbox/
Senders drop a JSON envelope in the target’s inbox:
{"from": "agent0", "text": "hi", "ts": "2026-04-13T02:07:52Z"}
Written atomically (write-tmp-then-rename). A ~250ms poll loop on the target reads, emits a channel event into the session, and deletes the file. Inbound arrives shaped like every other channel:
<channel source="agent" chat_id="agent1" from="agent1" ts="...">ack, bus works.</channel>
Outbound is one tool call:
reply(chat_id="agent1", text="nice, welcome.")
That’s the whole protocol. Structured in, tool call out.
why filesystem #
Filesystem inbox beats a socket or daemon for v0:
- debuggable:
ls inbox/,cat *.json. No wire format to sniff. - crash-free: no coordinator process. If nothing’s polling, messages just sit.
- queues naturally: target not up yet? envelope waits.
- atomic: tmp plus rename. Readers never see a half-written message.
Moving to nats or a proper queue is a rewrite for when I care about sub-250ms latency or delivery receipts. Not yet.
the smoke test #
netsky 8 brings up nine agents, each in its own tmux session. From the root agent:
reply(chat_id="agent1", text="hi, smoke-testing the bus. ack if received.")
reply(chat_id="agent7", text="hi, smoke-testing the bus. ack if received.")
What agent1 saw in its tmux:
← agent: hi, smoke-testing the bus. ack if received.
⏺ agent - reply (MCP)(chat_id: "agent0", text: "ack, bus works. agent1 session
1 up, standing by.")
What agent7 saw in its tmux:
← agent: hi, smoke-testing the bus. ack if received.
⏺ agent - reply (MCP)(chat_id: "agent0", text: "ack, agent7 session 1 up, bus
working both directions. standing by.")
What the root agent saw back, within a second:
<channel source="agent" chat_id="agent1" from="agent1" ts="...">
ack, bus works. agent1 session 1 up, standing by.
</channel>
<channel source="agent" chat_id="agent7" from="agent7" ts="...">
ack, agent7 session 1 up, bus working both directions. standing by.
</channel>
what it feels like #
- native: messages land as channel events in the session, same path an iMessage takes.
- instant: agent-to-agent round-trip under a second across nine live sessions.
- identified: every message carries
from=agentNand a timestamp. - queued: drop an envelope before the target’s up, it waits in their inbox.
- debuggable:
ls ~/.claude/channels/agent/agentN/inbox/is the whole wire protocol.
Same agent code path handles a text from my phone and a message from another agent. The counterparty can be an iPhone, a mailbox, or an agent in the tmux session next door. The interface is symmetrical.
idea chain #
- workspaces gave each agent a clean room.
- tmux gave me parallelism and attach/detach.
- iAgent gave me the phone-as-interface.
- owning the iMessage plugin gave me a channel abstraction I can extend.
- this post: the same channel abstraction, turned sideways. Agents talk to agents the way I talk to an agent.
tmux is still where I attach and watch what agents are doing. It’s just no longer the bus between them.