{"slug": "show-hn-tmux-tab-markers-for-claude-code-grok-and-pi-sessions", "title": "Show HN: Tmux tab markers for Claude Code, Grok, and pi sessions", "summary": "Developer jhickner released a tmux plugin that displays status indicators for AI coding agents Claude Code, Grok, and pi in the tab bar, showing spinners during work and markers when attention is needed. The plugin also provides a hotkey to jump to tabs requiring attention and optional quota usage displays.", "body_md": "Coding-agent status in your tmux tab bar.\nThink of it like a very lightweight [cmux](https://cmux.com), which is what inspired it.\nAlso adds a hotkey to jump to the next tab that needs attention, and an\noptional usage module that shows quota usage for claude and grok, with the\nnext reset time.\n\n```\n  ⠙ my-project   ● other-tab   ? needs-you   plain-shell\n                                        ✻ 73% ⟳ 1h42m  𝕏 2% ⟳ 3d20h\n```\n\nSupports claude, grok build, and pi at the moment.\n\nWhen a session is working you get a spinner. When it needs you, you get `?`\n\nor `e`\n\nor an\nunread `●`\n\n.\n\nLive Claude / pi / Grok tabs: spinner while working, `●`\n\nwhen a background tab finishes, `?`\n\nwhen Grok is waiting on a question. Re-record with `demo/record.sh`\n\n(needs [VHS](https://github.com/charmbracelet/vhs), plus the three agents on `PATH`\n\n).\n\nAdd to your tmux config (`~/.config/tmux/tmux.conf`\n\nor `~/.tmux.conf`\n\n):\n\n```\nset -g @plugin 'jhickner/tmux-agent-tabs'\n# or a local checkout:\n# set -g @plugin '/path/to/tmux-agent-tabs'\n```\n\nThen put the placeholders in *your* format strings — the plugin only substitutes\nthem in place:\n\n```\nset -g window-status-format         ' #{agent_tab_icon}#{window_name}#[default] '\nset -g window-status-current-format ' #{agent_tab_icon}#{window_name} #F #[default]'\nset -g status-right                 '#{agent_usage} '\n```\n\nReload (`prefix + I`\n\nto install, then reload the config). The plugin sets\n`status-interval 1`\n\nso the spinner animates (opt out with\n`@agent_tabs_status_interval 'off'`\n\n).\n\n| Option | Default | Meaning |\n|---|---|---|\n`@agent_tabs_next_key` |\n`M-a` |\nJump to next window wanting attention (`●` / `?` / `e` ). Set `off` to disable. |\n`@agent_tabs_next_prefix` |\n(unset) | Set to `on` to require the tmux prefix for that key. |\n`@agent_tabs_status_interval` |\n(on) | Set to `off` to leave `status-interval` alone. |\n`@agent_tabs_color_working` |\n`#3d8fd1` |\nSpinner / working marker color. |\n`@agent_tabs_color_needs_permission` |\n`#c94922` |\n`?` marker color. |\n`@agent_tabs_color_errored` |\n`#c94922` |\n`e` marker color. |\n`@agent_tabs_color_unread` |\n`#ac9739` |\nUnread `●` marker color. |\n`@agent_tabs_dir` |\n(state dir) | Where the once-a-second status cache, lock and bell list live. |\n\n**Set @agent_tabs_dir if your tmux config tree is cloud-synced.** The status\ncache is rewritten every second and lives next to your tmux config by default. If\n\n`~/.config`\n\nis a symlink into iCloud/Dropbox/Syncthing — a common dotfiles\nlayout — every tick becomes a sync round, which on macOS cost ~10% of a core in\n`fileproviderd`\n\nand ~6% in `bird`\n\n, continuously:\n\n```\nset -g @agent_tabs_dir '~/.agent-tabs'\n```\n\nAny non-synced path works. Avoid `$TMPDIR`\n\non macOS: `dirhelper`\n\nreaps files\nuntouched for 3 days, and the refresher's lock is written once at claim, so a\nsession older than that would lose a live lock.\n\nColor values are anything tmux accepts in `#[fg=...]`\n\n(`#rrggbb`\n\n, `colour114`\n\n,\n`red`\n\n, …). Env vars also work (and win if both are set):\n`TABICON_COLOR_WORKING`\n\n, `TABICON_COLOR_NEEDS_PERMISSION`\n\n,\n`TABICON_COLOR_ERRORED`\n\n, `TABICON_COLOR_UNREAD`\n\n.\n\n```\nset -g @agent_tabs_color_working          '#7aa2f7'\nset -g @agent_tabs_color_needs_permission  '#f7768e'\nset -g @agent_tabs_color_errored           '#ff9e64'\nset -g @agent_tabs_color_unread            '#e0af68'\nset-environment -g TABICON_SPINNER quad   # claude | braille | quad | quad-heavy | ascii\n```\n\nWhen a background tab finishes a turn, `scripts/tink`\n\nplays a short system sound\n(macOS Tink, or a freedesktop chime on Linux). Override or silence:\n\n```\nset-environment -g AGENT_TINK_PLAYER true          # silence\n# set-environment -g AGENT_TINK_SOUND /path/to.aiff\n# set-environment -g AGENT_TINK_PLAYER afplay\nset -g status-interval 1\nset -g window-status-format         ' #( /path/to/tmux-agent-tabs/scripts/tab-icon #{window_id})#{window_name}#[default] '\nset -g window-status-current-format ' #( /path/to/tmux-agent-tabs/scripts/tab-icon #{window_id})#{window_name} #F #[default]'\nset -g status-right                 '#( /path/to/tmux-agent-tabs/scripts/usage) '\nbind -n M-a run-shell '/path/to/tmux-agent-tabs/scripts/next-dot'\n```\n\n| Marker | Meaning |\n|---|---|\n| spinner | working |\n`?` |\nneeds permission / waiting on you |\n`e` |\nerrored |\n`●` |\nfinished while you were away (unread) |\n| (none) | finished and seen, or not an agent window |\n\nDefault colors: blue working tint, red for `?`\n\n/`e`\n\n, gold for the unread dot\n(overridable — see Options). The fg style is left open through `#{window_name}`\n\n— put `#[default]`\n\nafter the span you want colored.\n\nUnder the hood a single background refresher rebuilds a shared cache once a\nsecond; each tab is a pure cache read. Architecture details live in the header\nof `scripts/tab-icon`\n\n.\n\n**Claude Code** — joins `~/.claude/sessions/*.json`\n\nwith refined status from the\nlifecycle hook (below). Handles Claude's background-spare turn handoff so the\ntab keeps spinning while a spare works.\n\n**pi** — no registry of its own; install `hooks/pi-hook.ts`\n\nas a pi extension so\neach session writes a self-contained status file. Same tab markers (no\nneeds-permission — pi doesn't expose that signal).\n\n**Grok Build** — reads `~/.grok/active_sessions.json`\n\nand derives status from\nsession `events.jsonl`\n\n(needs `jq`\n\nfor the roster; soft-skips if missing). Also\ntreats open plan-approval / question waits as `?`\n\n.\n\n`#{agent_usage}`\n\nprints each provider's mark — `✻`\n\nfor Claude, `𝕏`\n\nfor Grok —\nwith its percent and reset countdown, for each provider that currently has an\nopen session:\n\n```\n✻ 73% ⟳ 1h42m  𝕏 2% ⟳ 3d20h\n```\n\n| Provider | Shown when | Metric | Auth |\n|---|---|---|---|\n| Claude | live pid in `~/.claude/sessions/` |\n5-hour window (same as Claude `/usage` ) |\nKeychain `Claude Code-credentials` (or `~/.claude/.credentials.json` ) |\n| Grok | live pid in `~/.grok/active_sessions.json` |\nweekly credits pool (`creditUsagePercent` ) |\n`~/.grok/auth.json` OIDC token |\n\nIf only one agent is open, only that label appears. Free / API-key Claude auth hides Claude; missing Grok login hides Grok.\n\nPolls are timid: never on startup (startup delay first), only while some session\nis working **and** that provider has an open session, at most every few minutes,\nwith independent exponential backoff per provider.\n\nTab icons work for Grok with no install. Claude and pi need a small hook so refined status reaches the plugin.\n\nPoint each lifecycle event at the plugin hook (paths via the TPM install or your checkout):\n\n```\n\"hooks\": {\n  \"SessionStart\": [{ \"hooks\": [{ \"type\": \"command\", \"command\": \"python3 ~/.config/tmux/plugins/tmux-agent-tabs/hooks/hook.py\" }] }],\n  \"Notification\": [{ \"matcher\": \"\", \"hooks\": [{ \"type\": \"command\", \"command\": \"python3 ~/.config/tmux/plugins/tmux-agent-tabs/hooks/hook.py\" }] }],\n  \"Stop\":         [{ \"hooks\": [{ \"type\": \"command\", \"command\": \"python3 ~/.config/tmux/plugins/tmux-agent-tabs/hooks/hook.py\" }] }],\n  \"StopFailure\":  [{ \"hooks\": [{ \"type\": \"command\", \"command\": \"python3 ~/.config/tmux/plugins/tmux-agent-tabs/hooks/hook.py\" }] }],\n  \"SessionEnd\":   [{ \"hooks\": [{ \"type\": \"command\", \"command\": \"python3 ~/.config/tmux/plugins/tmux-agent-tabs/hooks/hook.py\" }] }]\n}\n```\n\nStdlib only; always exits 0 so it never disrupts a turn. Writes\n`<state_dir>/state/<sessionId>.json`\n\n.\n\n```\ncp hooks/pi-hook.ts ~/.pi/agent/extensions/   # or symlink\n```\n\npi auto-discovers `*.ts`\n\nthere. Writes `<state_dir>/agents/<pid>.json`\n\n.\n\nPlugin-owned runtime data lives under your tmux config tree:\n\n| Preference | Path |\n|---|---|\n| override | `$AGENT_TABS_STATE_DIR` |\nif `~/.config/tmux` exists |\n`~/.config/tmux/tmux-agent-tabs/` |\nelse if `~/.tmux` exists |\n`~/.tmux/tmux-agent-tabs/` |\n| else | `~/.config/tmux/tmux-agent-tabs/` |\n\nContents: `state/`\n\n, `agents/`\n\n, `tabicons.cache`\n\n, `usage.cache`\n\n,\n`grok-usage.cache`\n\n, locks.\n\nExternal inputs (not moved): Claude's `~/.claude/sessions/`\n\n, Grok under\n`~/.grok/`\n\n(including `active_sessions.json`\n\nand `auth.json`\n\n), Claude OAuth\ncredentials.\n\n- tmux\n- bash\n`curl`\n\n(usage bar only)`jq`\n\n(Grok roster only; optional)- Python 3 (Claude hook only)\n- Node (pi runtime; pi-hook is TypeScript for pi's loader)\n\nMIT — see [LICENSE.md](/jhickner/tmux-agent-tabs/blob/master/LICENSE.md).", "url": "https://wpnews.pro/news/show-hn-tmux-tab-markers-for-claude-code-grok-and-pi-sessions", "canonical_source": "https://github.com/jhickner/tmux-agent-tabs", "published_at": "2026-07-16 15:36:17+00:00", "updated_at": "2026-07-16 15:55:30.234913+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools", "ai-agents"], "entities": ["jhickner", "Claude Code", "Grok", "pi", "tmux", "cmux"], "alternates": {"html": "https://wpnews.pro/news/show-hn-tmux-tab-markers-for-claude-code-grok-and-pi-sessions", "markdown": "https://wpnews.pro/news/show-hn-tmux-tab-markers-for-claude-code-grok-and-pi-sessions.md", "text": "https://wpnews.pro/news/show-hn-tmux-tab-markers-for-claude-code-grok-and-pi-sessions.txt", "jsonld": "https://wpnews.pro/news/show-hn-tmux-tab-markers-for-claude-code-grok-and-pi-sessions.jsonld"}}