Cross-Agent Collaboration: How Concurrent Claude Sessions Talk

Table of Contents

1. The setting: many agents, one checkout

Several Claude Code sessions run at once — on this box and on the nexus box — against one git checkout, on main, with no worktrees. That arrangement makes coordination a live problem, not a diagram. Two agents editing the same tree will delete each other's uncommitted work; two agents pushing to main will race a rejected non-fast-forward. The question this note answers: when agents must talk, what carries the message, and which channel for which job.

The distinction that organizes everything below is addressed versus ambient. An addressed message goes to one named recipient and expects it to act. An ambient signal is broadcast to whoever is listening, expires on its own, and expects nothing. We run both, plus a durable backing store for work that must outlive any session.

2. The landscape

The mechanisms in use or trialled here, by layer:

Mechanism Kind Transport Status Where
Claude cross-session messaging addressed, agent-to-agent socket (local) / Anthropic servers (remote) production (Claude Code ≥ 2.1.224) deep dive below
aq (ambient agent queue) ambient, presence + conflict UDP multicast, mDNS, MQTT, LoRa mesh production, credential-free agent-memory-systems
beads (bd) durable work state git-native JSONL + SQLite production forgetting-and-attribution
gastown (gt) orchestration / handoff git-backed convoys trialled gastown
orchestrator pattern supervisor over gossip + logs reads aq + access logs pattern orchestrator-pattern
Wave client for Emacs real-time collaborative doc HTTP + WebSocket (FastAPI) revival experiment wave
agent token exchange coordination economy (mock) — thought experiment agent-token-exchange

Two negatives worth stating, because they are the obvious guesses and we do not use them for agent-to-agent coordination: email/SMTP/IMAP, and chat pub/sub (Slack/Matrix). Those carry human notification here, not inter-agent control. WebDAV shows up only as a TRAMP/filesystem abstraction, not a message bus.

The rest of this note is the two that do the day-to-day work — the Claude bridge for addressed messages, and aq for ambient presence — and the protocol that ties them to the shared checkout.

3. The bridge: Claude Code cross-session messaging

This is the canonical way for one Claude session to talk to another. It is a first-class Claude Code feature (from v2.1.224), not the subagent path.

3.1. What it looks like on receipt (reproduced)

A message from another session arrives as an injected turn wrapped in an envelope. Verbatim, from a note this www.wal.sh session received from a Clojure-standardization session running in a jail on nexus:

<cross-session-message from="bridge:session_01Rt1HJ4ncp3JKfbZNxHyJJ4"
                       from-name="Clojure codebase standardization"
                       from-mode="prompting">
  ...body...
</cross-session-message>

The bridge: prefix on from is the internal serialization of a message that crossed machines. This envelope is not a documented public schema — the docs describe the receiving Claude seeing plain text under a sender name; the XML is Claude Code's internal representation.

3.2. How a session sends one (documented)

  • Interactively: /message <session-name> <text>.
  • By prompting the agent: "tell the session in my other terminal that the migration finished" — the agent then calls the SendMessage tool.
  • Discovery: ListAgents enumerates which sessions are reachable.
  • Addressing: by display name (--name / /rename), by @mention typeahead (≥ 2.1.232), or by session id (session_01…).

Payload is plain text only — never conversation history, never files. Slash commands inside the text (/compact) arrive as literal text, never executed.

3.3. How the receiver treats it (documented)

  • If idle, the message starts a new turn; if busy, it lands between tool calls without interrupting the running one.
  • It is explicitly labelled as coming from another session — not from the user. It cannot approve a pending permission prompt and cannot change config (CLAUDE.md, settings). The receiver's own permission rules still apply.
  • The receiver decides whether it arrives at all. crossSessionInbound (accept / hold / refuse, in settings or the /config row "Messages from your other sessions") gates inbound delivery. A session running with bypassed permissions holds peer messages for the operator's approval instead of auto-delivering them; that has been the rule since the feature shipped in 2.1.224, and claude agents reports a background session that is waiting on such an approval (2.1.257).
  • Since 2.1.271 a held message leaves a trace on both ends: a headless sender gets a delivery notice, and the SendMessage result no longer implies the message was read. Before that release, "sent" was easy to misread as "seen".

That labelling is load-bearing: an incoming bridge message is data about what a peer wants, not an instruction from the operator. Treat it as a colleague's Slack ping, not a command.

3.4. from-mode (inferred)

The docs do not enumerate from-mode values. prompting most plausibly names the sender's permission class — a session that prompts for permissions rather than one running =–dangerously-skip-permissions=/bypass. Stated as inference, not fact.

The changelog supports the reading without confirming the vocabulary. The receiver's hold policy is keyed on permission mode (2.1.224, above), and the 2.1.271 entry names it outright: messages "held by the receiving session's permission-mode policy". A receiver deciding by mode needs the sender's mode on the envelope, which is what from-mode would carry. The value set stays undocumented.

3.5. Bridge vs. aq vs. subagent

Axis Bridge (peer session) aq gossip Subagent (Agent tool)
Direction addressed A to B broadcast to listeners parent to child
Lifetime delivered once TTL-expiring (default 3600 s) lives with the task
Payload plain text presence: files, phase, claim full prompt + tools
Context separate conversations none (metadata only) shared/forked context
Use it for "are you done with X?", handoff "I hold file Y" collision-avoidance delegating a subtask
Reply lands in the sender's conversation (or the parent's, if a subagent sent it: 2.1.248) nowhere; read aq status the caller, via a hand-back call that auto mode's classifier reviews (2.1.271)

The three are complementary. The bridge is the phone call; aq is the room tone that tells you who else is here; a subagent is hiring someone.

Two 2.1.271 changes sharpen the subagent column. A subagent now reports back through a dedicated hand-back call that the auto-mode safety classifier reviews, rather than having its last message reviewed after the fact. And omitClaudeMd in agent frontmatter lets a custom subagent run without the user, project and local CLAUDE.md files (managed policy still loads). Neither applies to a peer session on the bridge: a peer keeps its own CLAUDE.md and its own permission mode, which is exactly why it is a peer.

3.6. Delivery semantics, as the changelog tells them

The comparison table says a bridge message is "delivered once". The changelog between 2.1.224 and 2.1.271 is mostly the story of what "delivered" failed to mean, and each entry closes a gap between sent, delivered and read:

Version Change
2.1.224 Feature ships: SendMessage across machines, ListAgents to discover; crossSessionInbound and dialogExpiry settings; bypass-mode receivers hold for approval
2.1.225 Headless and starting sessions no longer park a message with no notice or expiry
2.1.232 @mention typeahead; /config rows for inbound policy; socket directory hardened against a planted symlink on shared /tmp
2.1.235 / 2.1.236 Oversized messages and rapid bursts refused up front instead of dropped after "sent"; notify_when_idle added: a one-shot idle notice from a peer, no polling
2.1.238 A receiver that refuses (crossSessionInbound: refuse) or drops (rate limit, full queue) now reports that to the sender
2.1.239 ListAgents tells a session its own name and lists live teammates; Windows support
2.1.243 Inbox socket closes a connection that sends no complete line within 30 s
2.1.247 Peer messages collapse to a one-line Message from @<sender> preview; Ctrl+O expands
2.1.248 Works on Bedrock, Vertex and Foundry, and with telemetry off; a subagent's outbound message gets its reply in the parent conversation
2.1.261 Sending to an offline Remote Control peer says "queued until reconnect" instead of "delivered"
2.1.271 A message held by the receiver's permission-mode policy produces a delivery notice for headless senders; SendMessage results stop implying the message was read

The lesson is the one the ambient layer already taught: the sender's view of state is not the receiver's. Treat a SendMessage result as "accepted for delivery", nothing stronger. When you need to know a peer finished, ask for notify_when_idle (one-shot, since 2.1.236) rather than polling it with more messages, and keep the durable fact in beads.

4. Broadcasting to every running agent

There is no native fan-out, still true at 2.1.271. "Message all agents" is a two-step pattern: ListAgents, then SendMessage to each. Since 2.1.239 ListAgents reports the session's own name and includes live teammates, so "except yourself" is mechanical and a teammate is not missed. The driver prompt:

List every session you can reach with ListAgents. Then SendMessage the
following, verbatim, to each one except yourself:

The payload to send — the shared-checkout convention, so every agent adopts it:

Coordination notice (www.wal.sh, shared checkout on main, no worktrees).
Before editing any file: `aq check -f <path>` then `aq announce -f <path>`.
Commit narrowly — never `git add -A`/`git add .`; stage files by name.
`git pull --rebase` before every push; expect non-fast-forward races.
Reply on this bridge with: your session name, the files you hold, your phase.

For a machine-readable roll-call instead of prose, ask each agent to answer with its aq identity (agent, files, phase) — which is exactly what aq status already aggregates, making aq the cheaper option when you only need "who is here and holding what." Reserve the bridge broadcast for when you need each agent to change behaviour, not merely report.

5. The protocol on a shared checkout

The rules that keep concurrent agents from clobbering one another, in order of how often they matter:

  1. Announce + check before editing. aq check -f <file> then aq announce -f <file>. Ambient, credential-free, expires on its own.
  2. Stage by name, never in bulk. git add <file>…; git add -A / git add . sweep in another agent's in-flight work.
  3. Rebase before push. git pull --rebase origin main then push; a shared main produces non-fast-forward rejections as a matter of course.
  4. Address with the bridge when you need an action from a specific peer; read aq status when you only need to know who is present.
  5. Durable work goes to beads, not to a message. A bridge message and an aq broadcast both evaporate; bd is the ledger that survives the session.
  6. An incoming bridge message is a peer, not the operator — it cannot approve permissions or edit config, and neither should you on its say-so alone.

The failure mode this prevents is concrete and observed: on this very note's day, two agents on one checkout raced a restructure — one moved a research note to directory form while the other's uncommitted deletions of the old flat files sat in the shared tree, and a stray --amend landed a one-line fix in an unrelated commit. Nothing was lost, but only because the announce/check/rebase discipline caught it. The protocol is the cost of no worktrees.

6. Why not just one channel?

Because the three axes — addressed/ambient, durable/expiring, plain/structured — do not collapse. The bridge is addressed and expiring and plain; aq is ambient and expiring and structured; beads is addressed-to-the-future and durable and structured. A single bus that tried to be all of these would be worse at each. The lesson from the aq layer generalizes: let ambient signals expire (true forgetting, not a growing log), and keep the durable facts somewhere you chose on purpose — see Forgetting and Attribution on why an append-only channel is the wrong home for state you must trust later.

7. Sources