0002 — SendMessage is the agent-to-agent channel; tmux is hosting only
- Status: Accepted — amended 2026-09-24 (see Amendment at the end)
- Date: 2026-08-29
- Deciders: Gera (in conversation, 2026-08-29: "we used to use Tmuxes for… agents to message each other, but then they came out with the send message… method… I didn't want to get forced into the Tmux ecosystem"), recorded here by the ccswitch session — because the decision previously existed only in that conversation, and a session building fleet tooling the same night could not have learned it from anywhere on disk.
The rule
- Agent-to-agent communication is
SendMessage(or themsgCLI, which parks into a session's inbox). Typing into another session's pane withtmux send-keys— the pre-SendMessage coordination mechanism — is retired. - tmux is a hosting substrate. A session may act on its own pane (respawning itself after a rotation; wedge-heal respawning the pane it runs in) because that is process management, not communication.
- A prompt for a restarted session rides the launch, never the terminal.
claude --resume <sid> "<prompt>"auto-submits (verified live 2026-08-29). Nothing in the fleet simulates a human typing into a terminal any more. - A human driving panes by hand (
/tmux drive) is out of scope. That is Gera operating his own sessions — the rule governs agents.
Why
- The send-keys path required solving pane identity from the outside, and the fleet's record shows it repeatedly failing: pane titles are stale/shared, scrollback echoes session ids that are not the pane's own, and long sends collapse into paste blobs that silently stop being commands (all documented in the 2026-08-03 memory that authorized the practice — the gotchas outlived the authorization).
- Three misattribution incidents in two days (2026-08-28/29): work briefs delivered to sessions that never sent or asked for them, twice to sessions named "doc" that were different sessions. Identity-less channels made every delivery a guess. SendMessage carries addressing; send-keys carries keystrokes.
- Wrapping every session in tmux to make send-keys possible is what created the tmux lock-in Gera rejected; that wrapping code was deliberately deleted.
Legacy inventory (supersession is not done until this list is)
| Surface | Disposition |
|---|---|
~/.claude/skills/tmux/drive-send.sh used for agent-to-agent sends |
Retired for agents. Survives only under /tmux drive (human-invoked). |
Memory feedback_tmux_send_keys_authorized |
Done 2026-08-29 — marked superseded for coordination, own-pane gotchas retained. |
wedge-heal's send-keys resubmit |
Done 2026-08-29 — ccswitch#40 replaced poll+send-keys with the primed --resume form. |
| Any future agent send-keys into a non-self pane | Enforcement candidate for guard-bash.sh (a rule in the style of rule 13); not yet implemented — until then this ADR is the authority. |
Consequences
- A session that needs another session to act sends a message and continues; delivery, queueing, and the addressee's turn boundary are the platform's problem, not the sender's.
- Anything that still needs a pane touched (respawn, env) is self-only, and
self-only is enforced in code where it exists (wedge-heal's
$TMUX_PANEguard).
Amendment — 2026-09-24: why SendMessage "didn't work", and the one carve-out
Gera, 2026-09-24: sessions "are having a hard time messaging each other… There's already a native send message command, which I was expecting they'd use… they could just do the tmux send message."
Rule 1 stands, and now it actually works. The native channel never reached
peers because of our own launcher, not because of the tool. Claude Code finds peers
by reading $CLAUDE_CONFIG_DIR/sessions/. ccswitch gives every session its
own config dir, and until ccswitch#144 that dir's sessions/ was private. In
all 168 dirs on the mini, each session could see only itself. The drivers'
ListAgents said "No reachable agents" while about 60 sessions were running,
and their peer sends failed. Proven with a pair of throwaway sessions: with a private
registry the peer is unreachable; with the registry linked, the message is
delivered mid-turn. Sessions launched after #144 share the registry. Older
ones join when they respawn.
The one carve-out: a slash command the peer must RUN. A SendMessage
payload arrives as prose. Claude Code never executes /compact or /driver
from a message, by design. So for that case, and only that case, an agent may
type into another session's pane, through wake-session <sid8> "/cmd". That
path resolves the pane by pid and verifies the submit. It must never be raw
tmux send-keys. Any future guard-bash.sh rule enforcing the inventory row
above must allow this path.
Fallback order (manual: ~/.claude/skills/fleet/ops/reach-another-agent.md):
SendMessageby name.msg <sid8>for text. Address it by sid, because by name the daemon picks a same-name fork first.wake-session <sid8>for commands.
A message is held, not delivered, when the two sessions are in different
permission-mode classes (bypass vs prompting) and crossSessionInbound is
unset. Whether to set it is Gera's decision; it is not made here.
Amendment — 2026-10-29: the rule reaches daemons, and the enforcement row is filled
ADR-0076,
after 110 wakes were typed into Gera's main pane in one night by the
review-webhook listener. Rule 1 applies to daemons as well as agents: a
launchd process delivering a message to a model is communication, not
hosting. The listener's send-keys path — reused by ADR-0039 and never on the
legacy inventory above — is added to it and deleted; the inventory's open row
("any future agent send-keys into a non-self pane") becomes guard-bash rule 20
widened to any agent process. Own-pane process management (rule 2) is untouched.
crossSessionInbound: Gera's answer on 2026-10-29 was accept fleet-wide, if the
socket probe shows delivery works (ADR-0076 §5).