Skip to content
andrew.dunn.dev

muxr

Source

muxr was a session manager for AI coding work built on top of tmux and written in Rust. Every session had a two-level address, <repo>/<campaign>, and that address had to mean the same thing in three places that don’t normally agree with each other: the tmux session name, a directory of campaign.md/log.md files on disk, and the AI runtime’s own notion of a conversation (Claude Code’s session id, tied to a working directory). muxr’s job was to create all three together and, when one of them changed, carry that change into the other two without leaving a dangling reference behind.

in orderONE COMMANDmuxr rename<repo>/<campaign>1 · TMUXsession namerename-session2 · DISKcampaigns dircampaign.md, log.md3 · RUNTIMEsession id/rename in the paneRESULTin syncone address
One muxr rename lands on all three places a session lives, tmux, disk, and the AI runtime, so the address means one thing everywhere.
ONE ADDRESS, THREE SURFACES THAT MUST AGREEONE ADDRESSmuxr<repo>/<campaign>one address, and one rename that has toland in all three placeslaunch · save · restoreTWO TRUST BOUNDARIESa value bound for a subprocess argumentis escaped; a value typed into a livepane must not beRECYCLE · UPGRADErecycle and upgrade wait on a sentinel theagent writes, not on inferred readiness1 · TMUX SESSIONtmux rename-sessionone call, and the least interesting of the three2 · ON DISKcampaigns/<slug>/campaign.md · log.mdrestore rehydrates from this pointer, never asession id3 · AI RUNTIME · RAW KEYSTROKESa session id, no API to calltied to a working directory/rename <new> sent into the pane as literal textany runtime through a TOML subprocess adapter:JSON in, JSON out, fail-closed
muxr held one address across three surfaces, renaming the tmux session, the campaigns directory and the runtime session id in that order, with escaping that flips between a subprocess argument and a live pane, and a sentinel the agent writes instead of readiness inferred from outside.

The tmux half of a rename is one call (rename-session). The interesting half was the runtime, because a rename or a resume there isn’t an API call, it’s keystrokes typed into a live pane. muxr rename did three things in order: rename the tmux session, move campaigns/<old>/ to campaigns/<new>/ on disk, and send the runtime’s own /rename slash command into the pane as literal text so the tool’s internal state matched too. Early on (v0.9.3) that last step shipped a bug worth remembering: the interpolated value was shell-escaped before being sent, correct for a subprocess argument and wrong for a keystroke stream. Shell-escaping wrapped a model name in single quotes, so Claude read /model 'claude-opus-4-7' and rejected the literal quote characters as part of the name. The fix split one interpolation function into two, one escaped for launch arguments and one raw for anything typed into a live pane, because those are two different trust boundaries that happened to share a string type.

Highlights

  • 3.0.0 replaced every per-runtime special case with one subprocess extension contract (JSON on stdin, JSON back on stdout, fail-closed), earned from real extractions rather than designed up front. Adding a new coding tool became a TOML block (bin, args, resume_args, session_discovery) instead of a code branch; the opencode adapter shipped as a worked example that never touched core.
  • muxr upgrade relaunched every live session onto a freshly installed binary in place, resuming the same conversation id, so a new Claude Code release didn’t mean closing and reopening every open session by hand.
  • A reboot once lost 10 of 11 sessions, because restore resumed by conversation id and a session recycled just before save was a zero-turn conversation with no transcript, so --resume dropped its pane to a bare shell. The fix: restore now always rehydrates from the on-disk campaign/log pointer, never an id.
  • A CI job enforced that every production file read routed through one module, so a stray raw read could never again reintroduce the bug it was written to catch: one unescaped quote in log.md had made the file unparseable, and the launch command silently rebuilt itself with an empty prompt, on a live session, with no error surfaced.
  • MIT licensed, published to crates.io as nomograph-muxr with prebuilt macOS and Linux binaries per release.

The three-way address was the part of muxr worth building as its own tool: a rename that happens once and lands correctly in tmux, on disk, and inside the runtime, instead of three renames done by hand and occasionally forgotten. Most of what grew up around that core, the extension contract, the readiness machinery, the TUI, was infrastructure for infrastructure, and its retirement note above is the rest of that story.

kit and rune were retired six weeks earlier in the same consolidation, for the same reason: three bespoke binaries maintaining a personal toolchain, where the surface anyone actually used was small enough to be a config file and a shim.


Created Mar 2026 · Archived Jul 2026
Retrospective →