Coding-agent status in your tmux tab bar. Think of it like a very lightweight cmux, which is what inspired it.
| icon | agent status |
|---|---|
| ⠙ spinner | working/busy |
| ▘ spinner | plain command running under scripts/busy |
| ? question mark | has a question / permission prompt |
| e | error |
| ● dot | complete (clears after first visit) |
Supports:
- claude code
- codex
- grok build
- pi
- any long-running command, via the
busyprefix
Hotkeys:
- jump to next error/question/complete tab
- jump to next tab with agent type (grok build, claude code, etc.)
Usage bar:
- shows remaining usage for Claude Code, Grok Build, Codex
- shows remaining credit balance for openrouter
- only visible when an agent of that type is visible
Example tmux status bar:
⠙ agent1 ⠙ agent2 ● agent3 ? agent4 shell
✻ 73% ⟳ 1h42m ◎ 18% ⟳ 5d2h 𝚇 2% ⟳ 3d20h ⑂ $20.468
Live Claude / pi / Grok tabs: spinner while working, ● when a background tab finishes, ? when Grok is waiting on a question. Re-record with demo/record.sh (needs VHS, plus the three agents on PATH).
Install (TPM)
Add to your tmux config (~/.config/tmux/tmux.conf or ~/.tmux.conf):
set -g @plugin 'jhickner/tmux-agent-tabs' # or a local checkout: # set -g @plugin '/path/to/tmux-agent-tabs'
Then put the placeholders in your format strings — the plugin only substitutes them in place:
set -g window-status-format ' #{agent_tab_icon}#{window_name}#[default] ' set -g window-status-current-format ' #{agent_tab_icon}#{window_name} #F #[default]' set -g status-right '#{agent_usage} '
Reload (prefix + I to install, then reload the config). The plugin sets
status-interval 1 so the spinner animates (opt out with
@agent_tabs_status_interval 'off').
Options
| Option | Default | Meaning |
|---|---|---|
@agent_tabs_next_key |
M-a |
Jump to next window wanting attention (● / ? / e). Set off to disable. |
@agent_tabs_next_prefix |
(unset) | Set to on to require the tmux prefix for that key. |
@agent_tabs_usage_click |
(on) | Click a usage segment to jump to a window running that provider. Set off to disable. |
@agent_tabs_status_interval |
(on) | Set to off to leave status-interval alone. |
@agent_tabs_color_working |
#3d8fd1 |
Spinner / working marker color. |
@agent_tabs_color_needs_permission |
#c94922 |
? marker color. |
@agent_tabs_color_errored |
#c94922 |
e marker color. |
@agent_tabs_color_unread |
#ac9739 |
Unread ● marker color. |
@agent_tabs_color_command |
#5f9e6e |
Spinner color for a busy-wrapped command. |
@agent_tabs_command_spinner |
quad |
Spinner style for a busy-wrapped command. |
@agent_tabs_dir |
(state dir) | Where the once-a-second status cache, lock and bell list live. |
Set @agent_tabs_dir if your tmux config tree is cloud-synced. The status
cache is rewritten every second and lives next to your tmux config by default. If
~/.config is a symlink into iCloud/Dropbox/Syncthing — a common dotfiles
layout — every tick becomes a sync round, which on macOS cost ~10% of a core in
fileproviderd and ~6% in bird, continuously:
set -g @agent_tabs_dir '~/.agent-tabs'
Any non-synced path works. Avoid $TMPDIR on macOS: dirhelper reaps files
untouched for 3 days, and the refresher's lock is written once at claim, so a
session older than that would lose a live lock.
Color values are anything tmux accepts in #[fg=...] (#rrggbb, colour114,
red, …). Env vars also work (and win if both are set):
TABICON_COLOR_WORKING, TABICON_COLOR_NEEDS_PERMISSION,
TABICON_COLOR_ERRORED, TABICON_COLOR_UNREAD, TABICON_COLOR_COMMAND.
set -g @agent_tabs_color_working '#7aa2f7' set -g @agent_tabs_color_needs_permission '#f7768e' set -g @agent_tabs_color_errored '#ff9e64' set -g @agent_tabs_color_unread '#e0af68'
Spinner style
set-environment -g TABICON_SPINNER quad # claude | braille | quad | quad-heavy | dot | ascii set -g @agent_tabs_command_spinner braille # same styles, for `busy` commands
Finish chime
When a background tab finishes a turn, scripts/tink plays a short system sound
(macOS Tink, or a freedesktop chime on Linux). Override or silence:
set-environment -g AGENT_TINK_PLAYER true # silence # set-environment -g AGENT_TINK_SOUND /path/to.aiff # set-environment -g AGENT_TINK_PLAYER afplay
Without TPM
set -g status-interval 1 set -g window-status-format ' #( /path/to/tmux-agent-tabs/scripts/tab-icon #{window_id})#{window_name}#[default] ' set -g window-status-current-format ' #( /path/to/tmux-agent-tabs/scripts/tab-icon #{window_id})#{window_name} #F #[default]' set -g status-right '#( /path/to/tmux-agent-tabs/scripts/usage) ' bind -n M-a run-shell '/path/to/tmux-agent-tabs/scripts/next-dot'
Tab markers
| Marker | Meaning |
|---|---|
| spinner | working |
| command spinner | a busy-wrapped command is running (own style + color) |
? |
needs permission / waiting on you |
e |
errored |
● |
finished while you were away (unread) |
| (none) | finished and seen, or not an agent window |
Default colors: blue working tint, red for ?/e, gold for the unread dot
(overridable — see Options). The fg style is left open through #{window_name}
— put #[default] after the span you want colored.
Under the hood a single background refresher rebuilds a shared cache once a
second; each tab is a pure cache read. Architecture details live in the header
of scripts/tab-icon.
Agents
Claude Code — joins ~/.claude/sessions/*.json with refined status from the
lifecycle hook (below). Handles Claude's background-spare turn handoff so the
tab keeps spinning while a spare works.
pi — no registry of its own; install hooks/pi-hook.ts as a pi extension so
each session writes a self-contained status file. Same tab markers (no
needs-permission — pi doesn't expose that signal).
Codex CLI — install hooks/codex-hook.py as a lifecycle hook. It records
working, permission-wait, finished, and cleanup transitions, plus the primary
quota reading Codex already writes to its local transcript. Codex does not
expose a reliable failure lifecycle event, so it does not synthesize e.
Grok Build — reads ~/.grok/active_sessions.json and derives status from
session events.jsonl (needs jq for the roster; soft-skips if missing). Also
treats open plan-approval / question waits as ?.
Plain commands (busy)
Prefix any long command with scripts/busy to give it the same tab treatment as
an agent turn — its own spinner while it runs, the finish chime and the unread
● when it ends in a tab you were not looking at:
busy make -j8
busy npm test
busy -n deploy ./deploy.sh prodPut it on your PATH (ln -s ~/.config/tmux/plugins/tmux-agent-tabs/scripts/busy ~/.local/bin/busy) or alias it. It is a transparent wrapper: exit status,
stdio and signals are the command's own, and outside tmux it is a plain exec.
| Flag | Meaning |
|---|---|
-n name |
label recorded for the run (default: the command name) |
--no-linger |
spinner only — no finish chime, no unread dot |
The tab shows the command spinner while it runs, then ● (or e on a non-zero
exit) until you visit that window — a run too short to catch mid-tick still
chimes and lights the dot. busy starts no helper process of its own: it writes
a state file, marks it "sticky" on exit, and the refresher (already running,
already tracking which window you are on) drops it the tick you visit the tab.
Multiple busy runs in one window are fine: the most attention-worthy marker
wins, so a running command still spins while an earlier one waits to be seen.
Usage readout (Claude + Codex + Grok + OpenRouter)
#{agent_usage} prints each provider's mark — ✻ for Claude, ◎ for Codex,
𝚇 for Grok, ⑂ for OpenRouter — with its percent and reset countdown, for
each provider that currently has an open session:
✻ 73% ⟳ 1h42m ◎ 18% ⟳ 5d2h 𝚇 2% ⟳ 3d20h ⑂ $20.468
| Provider | Shown when | Metric | Auth |
|---|---|---|---|
| Claude | live pid in ~/.claude/sessions/, or a direct wrapper record |
5-hour window (same as Claude /usage) |
Keychain Claude Code-credentials (or ~/.claude/.credentials.json) |
| Codex | live pid from the Codex hook or a direct wrapper | primary rate-limit window | local transcript or app-server (no credential or request) |
| Grok | live pid in ~/.grok/active_sessions.json, or a direct wrapper record |
weekly credits pool (creditUsagePercent) |
~/.grok/auth.json OIDC token |
| OpenRouter | a live pi session whose model provider is openrouter |
remaining prepaid credit balance (/api/v1/credits), to tenths of a cent |
$OPENROUTER_API_KEY (read from the tmux env) |
If only one agent is open, only that label appears. Free / API-key Claude auth hides Claude; API-key Codex sessions with no subscription quota hide Codex; missing Grok login hides Grok; no OpenRouter-routed pi session hides OpenRouter.
Each segment is clickable: clicking a provider's mark/percent jumps to a window
running that provider's session (switching sessions if it lives in another one).
Clicking a window tab still selects it as usual. Disable with
set -g @agent_tabs_usage_click 'off'.
Claude, Grok, and OpenRouter network polls are timid: never on startup (startup delay first), only while some session is working (or just finished through a direct wrapper) and that provider has an open session, at most every few minutes, with independent exponential backoff. Codex makes no poll: its hook captures an already-local reading.
Hooks (Claude + Codex + pi)
Tab icons work for Grok with no install. The other agents need a small hook or extension so refined status reaches the plugin.
Claude Code
Point each lifecycle event at the plugin hook (paths via the TPM install or your checkout):
"hooks": { "SessionStart": [{ "hooks": [{ "type": "command", "command": "python3 ~/.config/tmux/plugins/tmux-agent-tabs/hooks/hook.py" }] }], "Notification": [{ "matcher": "", "hooks": [{ "type": "command", "command": "python3 ~/.config/tmux/plugins/tmux-agent-tabs/hooks/hook.py" }] }], "Stop": [{ "hooks": [{ "type": "command", "command": "python3 ~/.config/tmux/plugins/tmux-agent-tabs/hooks/hook.py" }] }], "StopFailure": [{ "hooks": [{ "type": "command", "command": "python3 ~/.config/tmux/plugins/tmux-agent-tabs/hooks/hook.py" }] }], "SessionEnd": [{ "hooks": [{ "type": "command", "command": "python3 ~/.config/tmux/plugins/tmux-agent-tabs/hooks/hook.py" }] }] }
Stdlib only; always exits 0 so it never disrupts a turn. Writes
<state_dir>/state/<sessionId>.json.
AGENT_TABS_WRAPPED=1 in the CLI's environment silences the hook. Set it from a
wrapper that drives Claude Code headlessly and reports the tab itself by writing
<state_dir>/agents/<pid>.json (simple-agent does): the SDK's interrupt ends a
turn with no Stop event, so the hook's record would stay on working for as
long as the wrapper lives. The wrapper is then responsible for removing any
<state_dir>/state/<sessionId>.json left from before it took over.
Direct wrapper records should include agent (claude, codex, grok, or
pi), pid, status, ts, and preferably tmux_pane. A Codex wrapper can
also publish usage_percent, usage_resets_at, usage_window_minutes, and
usage_ts; the usage readout consumes those fields without a hook or network
request.
Codex CLI
Merge these entries into ~/.codex/hooks.json, changing the checkout path if
needed:
{
"hooks": {
"SessionStart": [{ "hooks": [{ "type": "command", "command": "python3 ~/.config/tmux/plugins/tmux-agent-tabs/hooks/codex-hook.py" }] }],
"UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "python3 ~/.config/tmux/plugins/tmux-agent-tabs/hooks/codex-hook.py" }] }],
"PermissionRequest":[{ "hooks": [{ "type": "command", "command": "python3 ~/.config/tmux/plugins/tmux-agent-tabs/hooks/codex-hook.py" }] }],
"PostToolUse": [{ "hooks": [{ "type": "command", "command": "python3 ~/.config/tmux/plugins/tmux-agent-tabs/hooks/codex-hook.py" }] }],
"Stop": [{ "hooks": [{ "type": "command", "command": "python3 ~/.config/tmux/plugins/tmux-agent-tabs/hooks/codex-hook.py" }] }],
"SessionEnd": [{ "hooks": [{ "type": "command", "command": "python3 ~/.config/tmux/plugins/tmux-agent-tabs/hooks/codex-hook.py" }] }]
}
}Codex asks you to trust non-managed hooks the first time it loads them. The hook
is stdlib-only, always exits 0, and writes
<state_dir>/agents/codex-<sessionId>.json. Quota extraction is deliberately
best-effort because Codex documents the transcript format as non-stable; missing
or changed quota data simply hides the Codex usage segment.
pi
cp hooks/pi-hook.ts ~/.pi/agent/extensions/ # or symlink
pi auto-discovers *.ts there. Writes <state_dir>/agents/<pid>.json,
recording the model id and provider (the provider is what lets the usage bar
tell an OpenRouter-routed pi session from a direct-provider one).
State directory
Plugin-owned runtime data lives under your tmux config tree:
| Preference | Path |
|---|---|
| override | $AGENT_TABS_STATE_DIR |
if ~/.config/tmux exists |
~/.config/tmux/tmux-agent-tabs/ |
else if ~/.tmux exists |
~/.tmux/tmux-agent-tabs/ |
| else | ~/.config/tmux/tmux-agent-tabs/ |
Contents: state/, agents/, tabicons.cache, usage.cache,
grok-usage.cache, openrouter-usage.cache, locks.
External inputs (not moved): Claude's ~/.claude/sessions/, Codex session
transcripts under ~/.codex/sessions/, Grok under
~/.grok/ (including active_sessions.json and auth.json), Claude OAuth
credentials.
Requirements
- tmux
- bash
curl(Claude/Grok/OpenRouter usage only)jq(Grok roster only; optional)- Python 3 (Claude and Codex hooks only)
- Node (pi runtime; pi-hook is TypeScript for pi's loader)
License
MIT — see LICENSE.md.
