A bash shim that impersonates the tmux CLI surface Claude Code's Agent Teams calls,
routing pane operations into real Neovim splits via the stock nvim --remote-expr RPC.
Three files, ~1230 LOC.
:terminal. Believes it's talking to real tmux.
Probes tmux -V, then issues new-session, split-window,
send-keys, break-pane… to lay out its swarm.$NVIM (its RPC socket path) into every
:terminal child. That's the back-channel the shim talks to its parent nvim over.tmux → bin/tmux · argv ↓-L/-S/-f…), short-circuits -V,
routes the subcommand. Each of the 16 handlers parses its flags and makes one RPC._lua_call for the bookkeeping queries:
exit-code mapping for v:true/v:false (state_has_session) and
newline-termination for lists (state_list_panes).
state_leader_pane is a hardcoded constant — no nvim contact.nvim --headless --server $NVIM --remote-expr "…" ↓--headless +
</dev/null keeps terminal escape probes out of captured stdout.v:lua.require'nvim-tmux'.METHOD(a,b,…) via nvim_expr.
One RPC round-trip per tmux subcommand.runtimepath on first
call (via luaeval, path riding in the _A argument), so
require('nvim-tmux') resolves even for non-plugin installs. Idempotent.vim.g.nvim_tmux. Pure bookkeeping.vim.g.nvim_tmux = t (nested writes don't persist)bin/ to vim.env.PATH and sets
NVIM_TMUX_NVIM_BIN = vim.v.progpath so :terminal children find the shim.
Auto-run by plugin/nvim-tmux.lua (14-line stub) unless
vim.g.nvim_tmux_disable.vsplit, win_gotoid,
:terminal, chansend, nvim_buf_delete — each teammate is a
terminal buffer in its own window.tmux invocation is a fresh short-lived process, so state must
live somewhere persistent: the long-lived nvim process itself. Once state lives in nvim, the code that
mutates it wants to live there too — bash shrinks to what it's good at (being the tmux-shaped argv parser
at the process boundary), and the single event loop gives atomicity, per-instance scoping, and lifecycle
cleanup for free.
tmux split-window -t 2:0.0 -h -P -F '#{pane_id}'| # | Where | What happens |
|---|---|---|
| 1 | bin/tmux | main() strips globals, routes to cmd_split_window,
which parses -t 2:0.0 -h -P -F '#{pane_id}' via the arg helper. |
| 2 | bin/tmux | _lua_call split_window "2:0.0" "h" "" → (first call only) _lua_ensure_rtp →
nvim_expr "v:lua.require'nvim-tmux'.split_window('2:0.0','h','')" forks the nvim CLI
against $NVIM. |
| 3 | init.lua § actions | split_window resolves the parent's winid (state lookup, leader fallback),
vim.fn.win_gotoid(), vim.cmd('vsplit'), grabs the new win_getid(). |
| 4 | init.lua § state | split_pane('2:0.0') bumps windows["2:0"].next_pane and creates
panes["2:0.1"]; set_nvim_binding('2:0.1', winid) records the window id.
All inside one event-loop turn — atomic. |
| 5 | nvim | The split is visible; the expression result "2:0.1" returns over the socket. |
| 6 | bin/tmux | cmd_split_window captures 2:0.1 and, because -P -F,
emit_format prints 2:0.1 — exactly what Claude expects from real tmux. |
vim.g.nvim_tmux (flat: ids are the keys){
next_session_idx: 3,
leader_winid: 1000, -- cached on first op
sessions: {
"swarm": { idx: 2, next_window: 1 }
},
windows: {
"2:0": { name: "leader", next_pane: 2 }
},
panes: {
"2:0.0": { nvim_winid: 1000, … },
"2:0.1": {
nvim_winid: 1003, -- which split
nvim_bufnr: 4, -- which buffer
nvim_chan_id: 2, -- chansend target
hidden: false,
hidden_session: null
}
}
}
S:W.P — session-idx : window-idx . pane-idx, e.g.
2:0.1. Since the id already says where a pane lives, the store is flat:
lookups are panes[pid], listings are prefix scans. The leader is always
1:0.0 (hardcoded constant). Indices are monotonic — never reused.break-pane sets
hidden=true and closes the window — the buffer + terminal job stay alive,
and the record stays under its id (so it keeps resolving) while
list-panes/count_panes skip it. join-pane re-splits,
loads the preserved buffer, and moves the record to its new id._lua_call runs an entire operation to completion inside
nvim's single-threaded event loop. Concurrent shim invocations can't interleave — no locks,
no tmp+mv, no state files.PATHplugin/nvim-tmux.lua auto-runs setup() on startup →
prepends bin/ to vim.env.PATH → every :terminal child (incl. Claude)
resolves tmux to the shim. Zero system changes — your login shell never sees it.