# Show HN: Tmux tab markers for Claude Code, Grok, and pi sessions

> Source: <https://github.com/jhickner/tmux-agent-tabs>
> Published: 2026-07-16 15:36:17+00:00

Coding-agent status in your tmux tab bar.
Think of it like a very lightweight [cmux](https://cmux.com), which is what inspired it.
Also adds a hotkey to jump to the next tab that needs attention, and an
optional usage module that shows quota usage for claude and grok, with the
next reset time.

```
  ⠙ my-project   ● other-tab   ? needs-you   plain-shell
                                        ✻ 73% ⟳ 1h42m  𝕏 2% ⟳ 3d20h
```

Supports claude, grok build, and pi at the moment.

When a session is working you get a spinner. When it needs you, you get `?`

or `e`

or an
unread `●`

.

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](https://github.com/charmbracelet/vhs), plus the three agents on `PATH`

).

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'`

).

| 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_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_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`

.

```
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'
set-environment -g TABICON_SPINNER quad   # claude | braille | quad | quad-heavy | ascii
```

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
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'
```

| Marker | Meaning |
|---|---|
| spinner | working |
`?` |
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`

.

**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).

**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 `?`

.

`#{agent_usage}`

prints each provider's mark — `✻`

for Claude, `𝕏`

for Grok —
with its percent and reset countdown, for each provider that currently has an
open session:

```
✻ 73% ⟳ 1h42m  𝕏 2% ⟳ 3d20h
```

| Provider | Shown when | Metric | Auth |
|---|---|---|---|
| Claude | live pid in `~/.claude/sessions/` |
5-hour window (same as Claude `/usage` ) |
Keychain `Claude Code-credentials` (or `~/.claude/.credentials.json` ) |
| Grok | live pid in `~/.grok/active_sessions.json` |
weekly credits pool (`creditUsagePercent` ) |
`~/.grok/auth.json` OIDC token |

If only one agent is open, only that label appears. Free / API-key Claude auth hides Claude; missing Grok login hides Grok.

Polls are timid: never on startup (startup delay first), only while some session
is working **and** that provider has an open session, at most every few minutes,
with independent exponential backoff per provider.

Tab icons work for Grok with no install. Claude and pi need a small hook so refined status reaches the plugin.

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`

.

```
cp hooks/pi-hook.ts ~/.pi/agent/extensions/   # or symlink
```

pi auto-discovers `*.ts`

there. Writes `<state_dir>/agents/<pid>.json`

.

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`

, locks.

External inputs (not moved): Claude's `~/.claude/sessions/`

, Grok under
`~/.grok/`

(including `active_sessions.json`

and `auth.json`

), Claude OAuth
credentials.

- tmux
- bash
`curl`

(usage bar only)`jq`

(Grok roster only; optional)- Python 3 (Claude hook only)
- Node (pi runtime; pi-hook is TypeScript for pi's loader)

MIT — see [LICENSE.md](/jhickner/tmux-agent-tabs/blob/master/LICENSE.md).
