cmux, eight weeks later: the two-hop PATH trap

A June post covered getting cmux set up so agent panes survive a reboot. Since then the setup has been running continuously, and three things went wrong that were worth filing (#30, #31, #32). Here is what held up and what did not.

How teammate tabs actually work

cmux claude-teams gives each teammate its own watchable cmux tab by impersonating tmux. It drops a shim at ~/.cmuxterm/claude-teams-bin/tmux:

#!/usr/bin/env bash
exec "${CMUX_CLAUDE_TEAMS_CMUX_BIN:-cmux}" __tmux-compat "$@"

and sets $TMUX to a synthetic socket path. No tmux server exists anywhere. Claude Code thinks it is talking to tmux; cmux answers, and each new-window becomes a tab.

It works well. tmux -V returns tmux 3.4, and tmux list-windows enumerates your cmux workspaces:

0 m&a
1 voitta
2 agent-teams
3 debedb

The catch is that the whole thing rests on PATH resolution, in two places.

Hop one: which tmux wins

If the launching process resolves tmux to real Homebrew tmux instead of the shim, real tmux tries to connect to a socket that was never a socket, and teammate spawn breaks outright.

On this machine Homebrew sat at PATH position 11 and the shim at 26. Both sources pushing it there were self-inflicted: my own cmux.json command re-prepended /opt/homebrew/bin, and so did .bash_profile. The fix is to prepend the shim directory in the workspace command.

Worth noting for anyone copying the June post: on cmux 0.64.16 cmux claude-teams now puts the shim at position 1 itself. The cmux.json prepend is belt-and-braces today, not the load-bearing fix. Which matters, because it is not what was actually broken.

Hop two: the shim’s own cmux

Look at the shim again. It execs bare cmux. And the cmux binary lives at /Applications/cmux.app/Contents/Resources/bin/cmux, which is not on a normal login shell’s PATH:

$ env -i HOME=$HOME /bin/bash -lc 'command -v cmux'
$

So winning hop one buys you nothing if hop two loses:

$ env -i HOME=$HOME /bin/bash -lc \
'export PATH="$HOME/.cmuxterm/claude-teams-bin:$PATH"; tmux -V'
/Users/gregory/.cmuxterm/claude-teams-bin/tmux: line 3: exec: cmux: not found

One symlink fixes it:

ln -s /Applications/cmux.app/Contents/Resources/bin/cmux ~/.local/bin/cmux
$ env -i HOME=$HOME /bin/bash -lc \
'export PATH="$HOME/.cmuxterm/claude-teams-bin:$PATH"; tmux -V'
tmux 3.4

I had filed that symlink as an ergonomics nit — scripts otherwise need CMUX="${CMUX_BUNDLED_CLI_PATH:-/Applications/…/cmux}". It was not a nit. A shim on PATH whose own dependency is off PATH fails in a way that reads as a tmux problem.

The diagnostic that saved the most time

A queued teammate and a shadowed shim look identical from outside: the pane sits there, nothing tabs. They are distinguished by one check.

If ps shows no __tmux-compat process ever appeared, the spawn never reached tmux, so PATH is not your problem. In my case the launcher pane was in manual mode on and the teammate sat at indefinitely, having called nothing at all. I would have spent the evening on PATH.

The other habit worth keeping: read the live process environment, not the shell’s.

ps -Eww -o command= -p <pid> | tr ' ' '\n' | grep -E '^(TMUX|PATH)='

A pane’s shell will happily report a PATH the long-running agent inside it never saw.

What about reboot survival?

That part held. Across a real reboot on 0.64.16, all 7 panes came back on their exact resume commands, none silently dropped — including a teams-named pane, which now carries an agent-hook resume binding it did not have before.

One caveat the June post did not mention: identity is not preserved. Comparing session-com.cmuxterm.app-previous.json against the live session file, workspaceId and panel id share zero values across a restart. Restore recreates everything by replaying resume bindings. That is the mechanism behind rescued tabs landing on Claude Code’s “is this a project you trust?” prompt — folder trust was scoped to a workspace id that no longer exists.

If you go diffing those files yourself, the identity key is workspaceId, not id. Key on id and every workspace collapses to None, which reads as “ids are stable” — precisely backwards. I made that mistake on the first pass.

Net

Two config lines, both one-time:

ln -s /Applications/cmux.app/Contents/Resources/bin/cmux ~/.local/bin/cmux
# and in cmux.json's teams command:
export PATH="$HOME/.cmuxterm/claude-teams-bin:$PATH"

The setup from June is still the setup. What eroded was the environment around it — which is the recurring theme with long-lived agent sessions: nothing breaks, things merely get reordered underneath you.

The metric that graded my orchestration a C, and what it was actually measuring

I let an AI proficiency scanner read thirty days of my Claude Code transcripts. It handed back a 349 out of 1000 — a C — and, more interestingly, two flat zeros: Planning: 0. Customization: 0. Agent dispatches: 5.

I spend most of my time running multi-agent squads and writing skills. So either I’m worse at my own workflow than I thought, or the instrument is measuring the wrong thing. It turns out the source is right there — Elastic-licensed, readable — so I did the thing the score didn’t credit me for, and read it.

What it actually measures

The scanner is a transcript-shape heuristic. Volume it gets right: 168 sessions, 11.3k messages, 524M tokens — all there. The trouble is the three dimensions meant to capture sophistication, each pinned to a single tool-name signal:

  • Orchestration is the literal tool named Agent — and “parallel agents” only counts when two or more Agent blocks fire in one assistant turn.
  • Planning is the ExitPlanMode tool. That’s the whole definition.
  • Customization is a Write or Edit whose path ends in SKILL.md / CLAUDE.md / .mcp.json.

Three proxies, each standing in for a competence, each satisfiable by clicking the corresponding button.

Why my real work scored zero

Regular readers know the shape of my orchestration skill: an architect derives the parallel set, then each issue gets a developer in its own git worktree, an adversarial reviewer, an SDET, a productivity engineer. That is the opposite of “two Agent calls in one turn.” The squads run as background agents and separate worktree sessions, each producing its own top-level transcript. From the scanner’s vantage, that isn’t orchestration — it’s a pile of independent user sessions. The one line that decides this, is_main = "subagents" not in path, doesn’t just miss cross-process orchestration; it actively penalizes it, because every coordinated sub-session inflates the denominator as a plain session.

Planning is worse, and funnier. My whole doctrine is start with a conversation, not a spawn — the hard, valuable artifact is the parallel set an architect reasons out after reading the issues. None of that touches ExitPlanMode. So a planning discipline more deliberate than the feature scores lower than one press of the Plan button. The map graded me on whether I visited a specific city, not on whether I arrived.

And Customization: I author SKILL.md files as routine work, but from worktree sub-sessions and through git and PRs — not via the Write tool under ~/.claude/skills/ in a session it happens to be watching. The scanner even records prs_opened; it just doesn’t spend it on the dimension that would have caught me.

The actual defect

This is a construct-validity failure, not a bug. Every proxy assumes competence lives inside one process boundary and expresses itself through a named UI feature. Mine lives across boundaries — worktrees, sibling sessions, gh — and in git history the instrument never opens. It measures feature adoption and calls it proficiency. Goodhart is right there: I could raise the number tomorrow by making my workflow worse — two Agent calls per turn, a ceremonial ExitPlanMode, a Write I don’t need — and the score would thank me for the regression.

What I’d change

Same thesis I keep landing on: the score is the spaceship; the missing instrument is telemetry that reads across boundaries. Concretely — correlate sibling worktree sessions plus gh pr create into a single orchestration event; score customization from committed SKILL.md diffs, not in-transcript writes; credit the gh issue view fan-out and the long analysis turn that precede a spawn as the planning they are. I’ve written it up as an issue against the plugin, and I’ll send the scanner half as a PR. The server-side scoring isn’t in the open repo, so I can hand them a better signal but not a better weight — which is its own small lesson about what “open source” buys you.

None of this makes the tool useless. A cheap proxy over free telemetry is a reasonable place to start; I’d have started there too. But a proxy has to know it’s a proxy, and this one reports a C with the confidence of a measurement. The gap between what it counted and what I did is, as usual, the only part with information in it.