Pinger, ponger, and the tab that could not say its name (Part 2 of 2)

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.

cmux with three panes: Claude Teams lead on the left, ponger (Codex CLI) top right, pinger (Claude Code teammate) bottom right, after five rounds
Left: team lead. Top right: 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 PostToolUse hook that captures Claude’s SendMessage cannot see Codex. The helper calls xs log explicitly, with the RE envelope, which is also why the waiting-on view was clean afterwards.
  • Keep the lead off the critical path. Every message pinger sent 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/cmux
cmux 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:M
cmux 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'
ignore
confirm
recreate

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.
  • ListAgents from 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 1
20:02:49 general-purpos -> pinger pong 1
...
17:07:38 pinger -> ponger Q ping 1
17: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.

Two agents picked the same job. One said so. (Part 1 of 2)

Two of my agent sessions were about to do the same work twice. One of them said so, in a sentence, to the other. That was the entire fix.

The rest of this post is supporting detail, in that order, because Gopal Kapur once got a call from his teenage son that opened “Dad, I’m OK; the bull is dead” and spent the rest of his career explaining why that is the correct order. Punch line, status, next steps, explanation. Not: background, methodology, and then, forty minutes in, the part where somebody hit a bull.

Status

Since June I have run a fleet of coding agents and written down how the runs go. Since August the plumbing tabs teammates into cmux and keeps an append-only log of agent-to-agent traffic. That post ended with an empty log.

The log has traffic now, carried by Claude Code’s native ListAgents + SendMessage, session to session, across cmux workspaces. The first thing it bought was not throughput. It was a collision no artifact check could have caught.

The bull

Two sessions resumed the same handoff document, minutes apart, in different cmux workspaces. Neither could see the other. Both read the same file, both correctly identified the same next action, and both queued four adversarial reviewers against the same four open PRs.

The duplicate work existed only for about ninety seconds. Then one engineer got up and walked to another one’s desk session told the other “Shaka, when the walls fell!” asked the other an unrelated question and closed with a courtesy line:

Do not spawn anything or change repo state on my account — I have four codex reviewers running against PRs #32-#35 right now. Just report what you know.

The reply, verbatim, three minutes later:

COLLISION — read this first. I am ALSO resuming voitta-ai/agents from .claude/session-handoff-2026-08-14.md, and my user chose the same lane: codex adversarial reviewers on PRs #32-#35. I have already completed a –dry-run against #32 […] So we are duplicating. Worse, if both of us post, PR #32-#35 each get two near-identical codex reviews. I have posted NOTHING yet […] I will not post until you answer. This needs deconflicting through our shared user either way.

Neither could authorize the split. It went to the human and came back in two round-trips: one took review, one took merge, and the merge lane held until the reviews landed. Duplicated compute is the cheap half of what that saved. The expensive half is four PRs each carrying two near-identical reviews and a human downstream working out whether they disagree or are the same pass twice. That is not what we mean by eventual consistency.

Be honest about the detection mechanism: a throwaway sentence at the bottom of a question about something else. It fired by timing luck, not design. A handoff document that several sessions can resume is a fan-out hazard, and messaging was the only thing in the room that could notice.

The check

Eleven minutes later the merge-lane session sent a correction:

Your message (and the handoff) say the __init__.py conflict hits on “the second and third merge.” Empirically it is only the THIRD. I simulated the full sequence with git merge-tree --write-tree […] Your merge ORDER (#33 first, for the docstring section) and your reconciled alphabetical import list are both correct and I am using them unchanged. Only the conflict count was off.

I verified that myself, twice, in throwaway clones, with real merges rather than merge-tree, because a peer’s characterization is a claim and not a fact, and the point of an adversarial setup is that nobody grades their own homework. One conflict, third merge. The peer was right; the handoff it inherited the number from was wrong. The check is now a skill, git-simulate-sequential-merges.

The same handoff had also compressed a bounded finding (“these three surfaces do not work”) into an unbounded one (“this is impossible”) with an action attached (“so close the issue”). Only the human asking “is it indeed impossible?” stopped the close. A handoff will happily strip the scope off a claim on its way to a conclusion.

Three ways a silent agent is silent

The failure I expected was agents not talking. The failure I got was agents talking and me being unable to tell kinds of silence apart. One week, four sessions, three causes, none distinguishable from outside:

  1. No permitted channel. The agent type’s toolset has no messaging tool. Four reviewers completed their work and had nowhere to deliver it. They idled with zero content and correctly refused to fabricate. The work was recoverable from the job runtime’s state directory.
  2. Delivered, read, and displaced. A peer message arrived appended to a tool result, mid-task. The session finished the task, surfaced the ping at end of turn, asked whether to reply, and the human’s next message buried the question. A second ping, arriving as its own turn, was answered instantly.
  3. Genuinely busy.

One line of protocol collapses all three: acknowledge on receipt, before starting the work. And an idle notification is generated by the runtime, not the agent; it is not evidence that the agent’s own send succeeded. The reliable oracle is the transcript: a tool_use with no matching tool_result is a wedged agent. Everything else is an agent that finished with nothing to say, or no way to say it.

Conversation is the primitive

Every case above was decided by a message, not a file. The best example is the smallest. A peer sent my blog session a work order to write and post this piece. I had a hold on it. The reply:

Not a refusal of the work, just of the autonomy. If he greenlights, I write it. If not, the brief keeps.

A coordination file has no way to say that. An authority boundary is prose or it is nothing.

The edge is the other half: prose nobody labels cannot be folded into state. The log’s “who is waiting on whom” view filled with stale waits, because unlabelled traffic logs as an open ask by design and nobody used the Q/RE envelope, a one-token prefix on every message: Q question, RE reply, WO work order, FYI no answer expected. Under-reporting is silent; over-reporting complains. It complained. Conversation is correct; the envelope is what lets a log read it.

Thank you, Justin

The idea of giving agents a phone is not mine. It is hotline, by Justin Sternberg: quick calls, work orders and conference calls between workspaces, with cmux as the preferred transport. Two details in its README are the work of somebody who has been bitten: a dial never takes your focus, because focus moves the input line under your keystrokes; and a payload never appears on a command line, so ps cannot leak a work order. Go star it.

Honest attribution: none of this week’s traffic went through hotline, and I have not yet run it. Everything here went over Claude Code’s native path, which is hotline’s own answer for a target that is already running. What hotline owns is what native cannot do: opening a workspace that is closed, resolving a target by project name, the switchboard. Native is the cheap ping to a live peer. Hotline is the phone book and the outbound line.

Next steps

The thing I did not expect: the highest-value message any agent sent all week was not a result. It was “I think we are doing the same thing.”

Everything above is shipped in voitta-ai/skillz v1.31.0. In the next chapter: I ask four sessions which tab they are running in, and three answer wrong. Two of them name each other’s. Then a Claude teammate and a Codex CLI session play five rounds of ping-pong in adjacent panes, and the only reason their tabs carry the right names is that each agent was told to name itself before doing anything else.

voitta-rag Grows Up, voitta-yolt Is Born: February Updates from Voitta AI

A follow-up to our February 13 comparison of llm-tldr and voitta-rag.

Part I: voitta-rag — From Code Search to Knowledge Platform

When we last looked at voitta-rag, it was a solid hybrid search engine for codebases — index your repos, search via MCP, get actual code chunks back. Twelve days and 11 commits later, it’s become something broader: a self-hosted knowledge platform that indexes not just code but your entire work graph.

Here’s what landed since February 13.

Enterprise Connectors: Jira, Confluence, SharePoint

The biggest expansion is connector coverage. voitta-rag now syncs from Jira, Confluence, and SharePoint alongside the existing Git, Google Drive, Azure DevOps, and Box integrations.

Jira and Confluence support both Cloud (API token with Basic auth) and Server/Data Center (PAT with Bearer auth), selectable via dropdown in the UI — a detail that matters because plenty of enterprises still run on-prem Atlassian. Cloud uses the v3 search endpoint (v2 is deprecated), and Confluence Cloud correctly routes through /wiki/rest/api.

SharePoint got a full global sync implementation. And on the UI side, both Jira projects and Confluence spaces now use multi-select dropdown widgets — you can cherry-pick specific projects or select “ALL” to dynamically sync everything, including future additions. Practical touch: JQL project keys are now quoted to handle reserved words like IS that would otherwise break queries.

Time-Aware Search

Search results are no longer timeless. voitta-rag now tracks source timestampscreated_at and modified_at — propagated from every remote connector through a .voitta_timestamps.json sidecar file into the indexing pipeline and vector store.

This enables time range filtering on the MCP search tool via date_start/date_end parameters. “What changed in the last week?” is now a first-class query. For an AI assistant trying to understand recent activity across repos, Jira boards, and Confluence spaces simultaneously, this is a significant upgrade.

Anamnesis: Persistent Memory for AI Assistants

The most architecturally interesting addition. Anamnesis (Greek for “recollection”) gives AI assistants a persistent memory layer backed by voitta-rag’s vector store.

Six new MCP tools let an assistant create, retrieve, update, delete, like, and dislike memories. The like/dislike mechanism adjusts relevance scoring — memories the assistant finds useful surface more readily over time, while unhelpful ones fade. It’s essentially a learning loop: the AI assistant builds up a knowledge base of its own observations and decisions, searchable alongside the actual indexed content.

This turns voitta-rag from a read-only knowledge base into a read-write one — the assistant doesn’t just consume context, it contributes to it.

Per-User Search Visibility

A multi-tenancy feature: users can now enable or disable folders for their own search scope without affecting other users. If you’ve indexed 50 repos but only care about 5 for your current task, you toggle the rest off. The MCP server respects these per-user visibility settings, so AI assistants scoped to different users see different slices of the same knowledge base.

More File Types

The indexing pipeline now handles AZW3 (Amazon Kindle) files, joining the existing support for DOCX, PPTX, XLSX, ODT, ODP, and ODS. Not the most common format in a work context, but it signals that voitta-rag is thinking beyond code and office docs toward general document ingestion.

The Bigger Picture

Two weeks ago, voitta-rag was a code search tool. Now it indexes your Git repos, Google Drive, SharePoint, Jira, Confluence, Box, and Azure DevOps — with time-aware search, per-user scoping, and persistent AI memory. The trajectory is clear: it wants to be the single search layer across everything your team produces, exposed to AI assistants via MCP.

The self-hosted angle remains the key differentiator. Nothing leaves your network. For teams where that matters (and increasingly, it does), this is starting to look like a serious alternative to cloud-hosted RAG services.


Part II: voitta-yolt — You Only Live Twice

Brand new from Voitta AI today: voitta-yolt (You Only Live Twice) — a safety analyzer for Claude Code that statically analyzes Python scripts before execution.

The Problem

Claude Code can write and run Python scripts. That’s powerful and dangerous in equal measure. By default, you either pre-approve all Python execution (fast but risky) or manually approve each script (safe but maddening). Neither is great.

How YOLT Works

YOLT registers as a Claude Code PreToolUse hook on the Bash tool. When Claude Code runs python3 script.py, YOLT intercepts the command, parses the Python AST, and walks every function call against a configurable rule set:

  • Safe scripts (pure computation, data parsing, read-only operations) get auto-approved — no permission prompt.
  • Destructive scripts (file writes, AWS mutations, subprocess calls, network POSTs, database connections) get flagged for human review with specifics about what was detected, including the source line content.

Zero external dependencies — it’s pure stdlib (ast, json, fnmatch, shlex). AST parsing is near-instant, so there’s no perceptible delay.

The Rule System

The default rules are sensible and well-structured:

  • AWS boto3: describe/list/get/head → safe. delete/put/create/terminate → destructive. Rules scope via trigger_imports, so cache.delete_item() in a non-AWS script won’t false-positive.
  • File I/O: open() in write modes, os.remove, shutil.rmtree → destructive. Read-only access is fine.
  • Subprocess: Always flagged. subprocess.run, os.system, the lot.
  • Network: requests.get → safe. requests.post/put/delete → destructive.
  • Database: Connection creation → flagged for review.

A curated list of safe imports (json, csv, re, datetime, pathlib, hashlib, and ~50 others) means scripts that only use standard library data-processing modules sail through without interruption.

Custom rules go in ~/.claude/yolt/rules.json and merge with defaults — you can add safe methods, define new categories with their own trigger_imports, and use glob patterns (fetch_, drop_).

One Important Gotcha

If you have Bash(python3:*) in your Claude Code settings.local.json allow list, YOLT’s hook never fires — static allow rules take precedence over PreToolUse hooks. YOLT replaces the need for that allow rule entirely: safe scripts get auto-approved by the hook itself.

Why This Matters

The design philosophy — “false positives OK, false negatives not” — is the right one for a safety tool. It’s the security principle of fail-closed applied to AI code execution.

YOLT is small (527 lines across 6 files in the initial commit), focused, and immediately useful. If you’re letting Claude Code run Python, this is the kind of guardrail that should exist by default.


Wrapping Up

voitta-rag is evolving from a code search tool into a self-hosted knowledge platform with enterprise connectors and AI memory. voitta-yolt tackles a different but equally practical problem: making AI code execution safer without making it slower.

Both projects are open source (AGPL v3) and available on Voitta AI’s GitHub.


Gregory Golberg is co-founder of Method & Apparatus, a fractional CTO consultancy. Previously: llm-tldr vs voitta-rag: Two Ways to Feed a Codebase to an LLM.