A Claude Code teammate and a Codex CLI session played five rounds of ping-pong across two cmux panes yesterday. Fifty seconds, ten messages, in order, exactly once. Both sides in the traffic log under their own names; both tabs titled correctly, pinger and ponger, the whole run.

ponger, Codex CLI. Bottom right: pinger, a Claude Code teammate, footer @pinger. Tab titles set by each agent naming itself.That last clause is the news. The previous post was about what agents said to each other. This one is about the plumbing that lets a human watch them say it, and the four things that cost me time so they need not cost you any.
The cross-runtime run
Setup: cmux 0.64.22, Claude Code 2.1.248, Codex CLI 0.148.0. The lead is a Claude session started with cmux claude-teams. pinger is a Claude teammate, spawned with Agent(name: "pinger"), which on this build opens its own pane. ponger is Codex, launched by the lead into a cmux new-split right pane with -a never and the working directory pre-trusted.
Transport is the honest caveat. Codex has no SendMessage, and Claude’s peer socket wants an auth handshake, so the cross-runtime hop is keystroke injection into the peer’s pane: a ten-line helper does xs log, then cmux send --surface <peer> "<text>", then cmux send-key --surface <peer> Enter. Each side reads the other’s surface ref from a file the peer wrote at startup. The Codex TUI accepted ping 3 as a normal user turn, ran one shell command, and went idle. So “agent-to-agent traffic” is no longer a Claude-only claim, with the caveat that this channel is terminal input, not a runtime messaging API. It is now the skill cmux-claude-codex-cross-runtime-messaging, helpers included.
Two things the run exposed:
- The Codex half is in the log only because it logs itself. The
PostToolUsehook that captures Claude’sSendMessagecannot see Codex. The helper callsxs logexplicitly, with theREenvelope, which is also why the waiting-on view was clean afterwards. - Keep the lead off the critical path. Every message
pingersent to the lead, including “ready” and “done”, was delivered in one batch twenty minutes later, queued behind the lead’s own busy turn. Send succeeded; delivery waited. The game was unaffected only because the ping-pong hop did not go through the lead. Watch a file or the traffic log instead.
Naming, at three layers
Every peer session I asked “which tab are you?” answered wrong. Three of four, and two of them reported each other’s. The gap was the same at three layers:
| layer | was | now |
|---|---|---|
| tab | select-pane -T is a silent no-op; tabs read @<agent_type> |
rename recipe below; upstream fix merged (cmux #10198), unreleased as of 0.64.22 |
| self | CMUX_TAB_ID == CMUX_WORKSPACE_ID, and both go stale on --resume |
cmux identify + cmux tree --all; skill cmux-session-self-identity |
| log | sender logged as agent type; two agents of one type are one sender | agent-traffic-log 1.2.0 reads agent_id (name@team) |
The identity gap sits upstream of every addressing error, not beside it. If a session cannot name its own tab, it cannot tell a human which tab to address, so the human has no reliable source either. Yes, I typed instructions into the wrong tab. The fix was one peer message forwarding them.
Reproduce it
The June setup post has the full ~/.config/cmux/cmux.json with the Command Palette actions and the per-team workspace commands. Everything below is what changed since, or what June never had.
1. The cmux CLI must be on PATH on its own. The tmux shim execs bare cmux, and the bundled binary is not on a login shell’s PATH. One symlink, shadows nothing:
ln -s /Applications/cmux.app/Contents/Resources/bin/cmux ~/.local/bin/cmuxcmux hooks setup
2. The Claude Teams workspace command, current form. Pin the teammate mode on the launcher and prepend the shim directory; drop restart entirely (see the trap below):
{ "name": "Claude Teams", "keywords": ["claude", "teams", "agents"], "workspace": { "name": "Claude Teams", "cwd": ".", "layout": { "pane": { "surfaces": [ { "type": "terminal", "name": "Claude Teams", "command": "bash -lc 'export PATH=\"$HOME/.cmuxterm/claude-teams-bin:/opt/homebrew/bin:/usr/local/bin:$PATH\"; exec cmux claude-teams --teammate-mode tmux'", "focus": true } ] } } }}
The matching palette action is unchanged from June ("type": "workspaceCommand", "commandName": "Claude Teams"). Then cmux reload-config, Cmd+Shift+P, “Open Claude Teams”. Always start a team from that palette entry, never from a resumed pane: a resumed pane has no shim on PATH, so its teammates spawn a real, invisible tmux server.
3. Claude Code settings. In ~/.claude/settings.json, pin the mode (on auto it can pick in-process, which gives no pane and wedges) and hook the traffic log:
{ "teammateMode": "tmux", "hooks": { "PostToolUse": [ { "matcher": "SendMessage", "hooks": [ { "type": "command", "command": "~/.local/bin/xs-hook" } ] } ] }}
Claude Code fires the parent’s hooks inside teammates too, so this one entry logs every SendMessage the whole team makes. In August the log depended on each agent remembering to call xs log; I predicted under-reporting, and this is the fix.
4. The log itself. xs and xs-hook live in agent-traffic-log/scripts; symlink both into `~/.local/bin`. Use 1.2.0 or later, or two agents of one type log as one sender. A traffic pane: cmux new-split right --command "xs tail".
5. The agents name their own tabs, because Claude Code cannot. Claude Code runs select-pane -T pinger right after spawning a teammate; cmux’s tmux-compat layer accepts that, returns 0, and does nothing, so every teammate tab falls back to @<agent_type>. The call that sticks is cmux tab-action --action rename --surface surface:N --title <name>, with surface:N read off cmux identify (caller.surface_ref), not off $CMUX_TAB_ID, which can alias the workspace id. No human ran it in the screenshot above. The lead renamed the Codex pane right after creating it and before launching Codex in it; the Claude teammate’s brief made pp-register pinger its first action, which resolves its own surface and renames it. Put the rename in the brief and the tabs are right from the first second of the run. cmux rename-workspace is the workspace, not the tab.
6. The Codex side. Create and name the pane before launching, then launch from a script typed into it:
cmux new-split right --workspace workspace:N --focus false # -> OK surface:Mcmux tab-action --action rename --surface surface:M --title ponger# in that pane:codex -a never -s danger-full-access -c 'projects."<dir>".trust_level="trusted"' "$(cat brief.md)"
danger-full-access is required because Codex’s sandbox blocks the cmux socket; keep the cwd a scratch directory. If no rollout file appears under ~/.codex/sessions/ within thirty seconds, Codex is parked on a startup modal you cannot see from outside: send one cmux send-key --surface surface:M Enter. The pp-send / pp-register helpers and the exact briefs are in cmux-claude-codex-cross-runtime-messaging.
7. Check, from inside the Claude Teams pane: which tmux resolves to $TMPDIR/cmux-cli-shims/<surface-uuid>/tmux (it moved there on 0.64.22; the old ~/.cmuxterm/claude-teams-bin check now fails on a healthy setup) and tmux -V prints tmux 3.4.
The trap that hides the palette entry
I added "restart": "restart" to the workspace command one afternoon. Valid values are ignore, confirm, recreate, or omit the key. An invalid one makes the command fail to load, which makes the palette action pointing at it unavailable, which hides it. Three hops from a typo, none logged. cmux config doctor says OK because it is a JSONC parser, not a validator. The oracle that worked: Swift enums land adjacent in the binary’s strings table.
$ strings -a /Applications/cmux.app/Contents/MacOS/cmux | grep -B3 -A3 '^recreate'ignoreconfirmrecreate
Also: restart governs what happens when a workspace of that name already exists. It restarts nothing, and omitting it is what lets you run two teams at once.
What you watch
- The panes. Each teammate is a real cmux pane, own TTY, full TUI. Footers read
@pinger/@ponger, right even when tab titles are not. - The leader’s agent list, bottom of its TUI, live elapsed time per teammate.
ListAgentsfrom any other session: address book and liveness view in one.cmux workspace list --json, which carries the last prompt every workspace received, readable without opening any of them.- A traffic pane:
cmux new-split right --command "xs tail".
The column from a Claude-only run, hook only, before the sender fix, and from yesterday’s cross-runtime run:
20:02:43 general-purpos -> ponger ping 120:02:49 general-purpos -> pinger pong 1 ...17:07:38 pinger -> ponger Q ping 117:07:42 ponger ok pinger RE pong 1
Shipped
voitta-ai/skillz, bundle v1.31.0: claude-code-cross-session-messaging, parallel-agent-session-collisions, cmux-cross-session-visibility, agent-traffic-log 1.2.0, subagent-no-report-channel, cmux-session-self-identity, git-simulate-sequential-merges, cmux-config-silent-drop-triage, cmux-agent-tabs 1.4.0, and cmux-claude-codex-cross-runtime-messaging.
Still open, and this is the whole list: select-pane -T stays a no-op upstream (they fixed naming by reading argv, not by honouring the flag); Codex stalls on a startup dialog that cmux’s CLI cannot read from outside, so a single Enter had to be sent blind; and cmux logs VaultAgentRegistry: Failed to decode config every eight seconds against a working config, undiagnosed and unrelated to any of the above.
Further work
Add Tamarian mode.