2026-07-27 Cross-Agent Detection and Waiting State in Tmux
What I set out to do
Answer whether the tmux agent session manager in nix/ finds Claude sessions outside Neovim, and whether it finds Codex and pi. Then, whether any upstream generalization already exists for the two underlying questions: is an agent in this pane, and is it waiting. It turned into shipping the thing.
What I actually did
Read claude-detect.sh, claude-pane-picker.sh, claude-status.sh, and the tmux-agent-state.sh hook. Direct (non-Neovim) Claude is found: comm equals claude, checked before the Neovim path, and the picker’s digit-led case accepts Claude’s version process title. Codex and pi are not, blocked at three layers: the pane_current_command allowlist, the hard-coded comm == "claude" test, and the Neovim RPC’s argv regex. pi is the harder case since it is a #!/usr/bin/env node script, so its comm is just node.
On generalization, found no standard for pane occupancy. The convergent mechanism is a hook tagging the pane through inherited $TMUX_PANE, which is what tmux-agent-state.sh already did and what tmux-agent-indicator, marmonitor, and tmux-agent-sidebar independently landed on. For waiting state there is a real convergence: Codex 0.145.0 adopted Claude Code’s hook event vocabulary wholesale.
Then built it. @claude_state became @agent_state carrying <agent>:<state>, the shared script moved to coding-agents/common and takes <agent> <state> on argv so it never parses JSON, and four agents now publish it: claude and codex with a real blocked state, agy and pi with running/idle only. Two commits, 04f38e0 and 848eee9.
What was striking
The bell test. I assumed OSC 777 would set window_bell_flag because it ends in BEL. It does not: tmux eats the BEL as the OSC string terminator, and only a bare \a flips the flag. The old Claude Code Terminal Notification System note corroborated this from the source side, noting every OSC channel is wrapped for DCS passthrough except BEL, which stays raw so tmux can trigger bell-action. Two independent routes to the same fact, years apart.
The agy adapter took four wrong turns and is the real lesson of the day, written up as Antigravity Hooks Take the Event as a Key. I guessed the global path (~/.agents/, wrong), wrote the event as a field instead of a key (parses fine, never fires), read “not logged into Antigravity” in the log and concluded it was unauthenticated (those are background pollers; it was logged in and answering prompts the whole time), and then blamed a nix store symlink for a failure that was really send-keys racing the TUI so the prompt never landed. Every one of those looked like a confirmed finding at the time, and three of them I had already reported as fact.
What made them all possible is the same thing: agy is a Go binary that ignores unknown JSON fields, so no wrong shape ever produces an error, and its startup log reports the file as loaded regardless. There was no failing case to observe. Nothing resolved it except asserting the actual side effect, which is Verify Tool Output Before Claiming Success restated. The reliable move, once I found it, was a marker file per candidate spelling and one real turn.
Also worth noting: agy has no session-start event at all, so a launched-but-unprompted pane publishes nothing and the picker has to fall back to matching the process name. pi has a real session_start, so it does not need that.
Remaining
- agy’s
.gemini/config/hooks.jsonis now correct, but the CLI’s own config dir silently swallows ahooks.jsonplaced there. Worth watching if Google consolidates the paths. - Gemini CLI is still unwired: npm-installed, no module. Its
Notificationcarriesnotification_type: "ToolPermission", so it would get a proper blocked state. - The statusline only counts tagged panes, so an unprompted agy shows in the picker but not the count.
Related
Agent Waiting State Splits Into Blocked and Idle, Antigravity Hooks Take the Event as a Key, Tmux Consumes the BEL That Terminates an OSC Sequence, Claude Code Terminal Notification System, Tmux capture-pane Color Semantics