claude-code-statusline
Statusline + notification hook + external watcher for Claude Code.
Tells you at a glance which of your parallel Claude Code windows is busy, idle, or waiting for permission — and surfaces toast notifications when one needs your attention.
Why
When you run several Claude Code sessions in parallel terminals, it's easy to lose track of which one is thinking, which one finished, and which is blocked on a permission prompt. The default TUI doesn't surface that across windows. This adds:
- A persistent statusline at the bottom of each Claude Code window with project name, busy indicator, model, context %, rate-limit usage, and elapsed-since-last-activity.
- A notification hook that fires distinct sounds and Windows toasts (with project name in the title) when a window wants you.
What it looks like
| Segment | Meaning |
|---|---|
project-name |
session_name if set, else cwd basename. Truncates with … to fit terminal width. |
● / ● THINKING / ● STUCK / ● WAITING |
Session classification. Green ● = busy (fresh). Yellow ● THINKING = busy + silent ≥60s. Red ● STUCK = silent ≥180s. Red ● WAITING = permission prompt pending. Hidden = idle. See Status colors for full details. |
Opus 4.7 1M |
Model display name; 1M suffix when 1M context is enabled. |
13% (131k) |
Context used: percent + token count. Dim → yellow ≥60 → bold yellow ≥80 → red ≥90. |
5h:19% 7d:48% |
Rate-limit usage (5-hour / 7-day). Dim → yellow → red as it climbs. |
last 12:34:56 |
Local wall-clock time of the last transcript event. Dim if <30s old at last paint → yellow <2min → red ≥2min. Frozen between Claude Code repaints by design — comparing a stale wall clock to your own is self-evidently stale, whereas a frozen "Ns" counter would lie. Run watcher.py for real-time ticking. |
Status colors
Both the statusline ● and the tray icon share the same colors and thresholds.
| Color | Statusline | Tray | Trigger | What to do |
|---|---|---|---|---|
| Green | ● |
green dot / count | Session is busy and the last non-thinking transcript entry is <60s old |
Nothing — Claude is healthily working |
| Yellow | ● THINKING |
yellow dot / count | Busy, last non-thinking entry is ≥60s old | Wait — likely a long thinking block or a slow tool call. Heads-up, not yet a problem |
| Red | ● STUCK |
red dot / count | Busy + silent ≥180s | Probably hung — consider interrupting |
| Red | ● WAITING |
red dot / count | status=waiting (permission prompt pending) |
Approve/deny the dialog in the Claude Code window |
| Gray | (hidden) | gray dot / 0-count | No sessions, or all sessions are idle |
Nothing |
Thresholds: SLOW_THRESHOLD=60s (yellow) and STUCK_THRESHOLD=180s (red), defined once in watcher.py and mirrored in statusline.py so the two surfaces never disagree.
"Last non-thinking entry": both surfaces walk the transcript JSONL backwards and skip subtype=="thinking" rows, so extended-thinking writes don't mask a session that's been silently reasoning. Falls back to file mtime if no parseable non-thinking entry exists (brand-new turn).
Where each color is computed:
- Statusline
●—statusline.py:render_status, computed only when Claude Code repaints (events only; therefreshIntervaltimer does not fire during silent stretches). - Tray icon —
watcher.py:overall_severity, computed every 300ms by the background poll loop, showing the loudest severity across all your Claude Code windows.
The notification hook adds:
- Permission prompt → warning beep + toast titled
[project] Permission needed - Idle prompt (turn ended, awaiting input) → chime + toast titled
[project] Awaiting your input - Elicitation dialog → asterisk beep + toast titled
[project] Question
The external watcher (watcher.py) is the answer to "is anything actually stuck?" — Claude Code refreshes the in-TUI statusline sparsely during silent work, so the dot can lie. Run the watcher in a separate terminal and it polls every session on its own schedule, flips sessions from ● BUSY → ⌛ THINK → ⚠ STUCK as transcripts stay silent, and beeps when a session escalates to STUCK or WAIT:
Claude Code Watcher · 2026-05-20 11:45:12
──────────────────────────────────────────────────────────────────────────────
PID STATE PROJECT ELAPSED
──────────────────────────────────────────────────────────────────────────────
25632 ● BUSY Add progress indicator 3s
28104 ⌛ THINK Other Project 1m22s
31840 ⚠ STUCK Doing Hard Thing 4m05s
19200 ▶ WAIT Yet Another 12s
──────────────────────────────────────────────────────────────────────────────
Run with:
python watcher.py # default 300ms poll
python watcher.py --no-sound # silent
python watcher.py --interval 1.0 # slower poll
Tray-icon mode (no extra terminal pane)
If you don't want the watcher TUI eating a terminal pane, run it headless with a system tray icon driven by the same classification logic:
pip install "claude-watcher[tray]"
claude-watcher --tray
Or from a checkout (no install), using the same venv:
.venv/Scripts/python -m claude_watcher.watcher --tray # Windows
.venv/bin/python -m claude_watcher.watcher --tray # macOS / Linux
The tray icon is a single colored circle whose color = the loudest severity across all your sessions (see Status colors).
Tooltip lists each session's classification. When a terminal window is the OS foreground, the most-recently-active session is sorted to the top with a ▶ marker — proxy for "the session you're probably looking at" (we can't read which tab inside Windows Terminal is active without UI Automation).
Right-click menu — Open action (opens the info window), session-summary entries (read-only, no action — present for visual reference with normal-color text), and Quit.
Info window — double-click the tray icon (or pick Open from the menu) to pop a small borderless light-themed window listing all sessions. Each session shows:
- Top row — colored dot, project name, status word; the focused session is bolded with
▶. - Detail row (muted, monospace) —
cpu N%(claude.exe CPU;—until psutil has two samples),age Xm Ys(transcript-mtime age, skipping thinking entries), andrunning: <tool>if anassistant/tool_useis still waiting on itstool_result. The CPU + tool combo disambiguates the silent-but-busy case: high CPU = thinking hard; near-zero with a tool name = blocked on that tool; near-zero with no tool = likely stuck on the API.
Auto-refreshes every ~700ms while open; closes on focus-loss or Esc. Rendered by a child process so it doesn't block the tray. CPU% requires the optional psutil dep (uv pip install psutil).
Toast on escalation — when a session crosses into ⚠ STUCK or ▶ WAIT, the watcher fires a desktop toast ([project] STUCK or [project] WAIT) in addition to the audible beep. Same backend per platform: WinRT on Windows, osascript on macOS, notify-send on Linux.
Beeps on STUCK/WAIT escalations (use --no-sound to silence).
Autostart on login
Register the tray watcher to launch automatically:
Windows: writes HKCU\Software\Microsoft\Windows\CurrentVersion\Run\ClaudeCodeWatcher pointing at a VBScript wrapper that invokes pythonw.exe -m claude_watcher.watcher --tray with a hidden window (uv-built venvs ship pythonw.exe as a trampoline shim that flashes a console without the wrapper).
claude-watcher --install-autostart
macOS: writes ~/Library/LaunchAgents/com.anthropic.claude-code-watcher.plist and launchctl loads it immediately, so the watcher starts now and at every login. The watcher hides itself from the Dock via NSApplicationActivationPolicyAccessory; only the menu-bar icon shows.
claude-watcher --install-autostart
First time the watcher fires a toast, macOS will prompt you to allow notifications for the Python interpreter (System Settings → Notifications). Grant it once.
Linux: writes ~/.config/autostart/claude-code-watcher.desktop (XDG autostart). Most desktop environments honor it on next login.
claude-watcher --install-autostart
To remove on any platform: claude-watcher --uninstall-autostart.
Auto-start when Claude opens (alternative to OS-login)
If you'd rather the tray only run while you're actually using Claude — not idle in the background between reboots — register a Claude Code SessionStart hook instead. The hook fires when you open a Claude session; a tiny launcher (claude_watcher.launch_watcher) checks via psutil whether a tray watcher is already alive and spawns one only if not. Subsequent session opens are no-ops.
claude-watcher --register-claude-hook
This adds an entry under hooks.SessionStart in ~/.claude/settings.json (with a .bak of the previous file). On Windows a small VBScript wrapper (~/.claude/_claude_watcher_hook.vbs) is generated so the launcher invocation is fully hidden; on macOS/Linux the command runs python -m claude_watcher.launch_watcher directly. Idempotent — re-running detects the existing entry and does nothing.
| OS-login autostart | SessionStart hook | |
|---|---|---|
| Trigger | At every login | First Claude session after a reboot |
| Watcher running when no Claude windows open | Yes (~30 MB RAM idle) | No |
| Startup latency on first Claude open | None (already running) | ~300 ms (pystray spin-up) |
| Setup command | --install-autostart |
--register-claude-hook |
Pick one. To remove the hook: claude-watcher --unregister-claude-hook.
Configurable thresholds
Override the classification thresholds via env vars (same vars read by claude_watcher.statusline so the two surfaces never disagree):
| Env var | Default | Effect |
|---|---|---|
CLAUDE_WATCHER_SLOW_SECONDS |
60 |
Age at which a busy session flips to ⌛ THINK (yellow) |
CLAUDE_WATCHER_STUCK_SECONDS |
180 |
Age at which it flips to ⚠ STUCK (red) |
Manual launch (without autostart)
Windows — use pythonw.exe (no console window):
pythonw -m claude_watcher.watcher --tray
macOS — claude-watcher --tray works but you'll see a Dock icon while it runs. Either use --install-autostart (which hides the Dock icon) or nohup to detach from the terminal:
nohup claude-watcher --tray > /dev/null 2>&1 &
Linux — same nohup pattern as macOS, or use systemctl --user for a real service.
Requirements
- Statusline + watcher: any OS with Python 3.10+ and Claude Code.
- Notification hook: cross-platform via auto-detected backend.
- Windows 10/11 — toasts via WinRT (needs the bundled AppId registration); sounds via
winsound.MessageBeep. - macOS — toasts via
osascript display notification; sounds viaafplayagainst/System/Library/Sounds/. - Linux — toasts via
notify-send(installlibnotify); sounds viapaplay(PulseAudio) oraplay(ALSA), falling back to terminal bell.
- Windows 10/11 — toasts via WinRT (needs the bundled AppId registration); sounds via
Install
Via pip (recommended)
pip install "claude-watcher[tray]" # tray extras pull pystray + pillow + psutil
claude-statusline --help # entry-point script for the statusline
claude-watcher --tray # entry-point script for the tray watcher
Then point Claude Code's statusLine.command at claude-statusline instead of python ~/.claude/statusline.py. To set up auto-start, run claude-watcher --install-autostart (OS-login) or claude-watcher --register-claude-hook (start on Claude session open) — see Autostart on login for the OS-specific details.
Automated (Windows, git checkout, legacy)
git clone https://github.com/EranDaniel98/claude-code-statusline.git
cd claude-code-statusline
.\scripts\install.ps1
# or .\scripts\install.ps1 -DryRun to preview without writing anything
The script:
- Copies
statusline.pyandhooks/notify.pyinto~/.claude/(backing up any existing files). - Registers the
Anthropic.ClaudeCodeAppId so Windows actually displays toasts (Windows silently drops toasts from unregistered AppIds). - Deep-merges
settings.example.jsoninto~/.claude/settings.jsonwith a.bakof your old file. Existing unrelated settings (other hooks, custom rules) are preserved.
Restart any open Claude Code window after install.
Manual (any platform)
-
Copy files into
~/.claude/:Windows (PowerShell):
Copy-Item statusline.py "$env:USERPROFILE\.claude\statusline.py" New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.claude\hooks" | Out-Null Copy-Item hooks\notify.py "$env:USERPROFILE\.claude\hooks\notify.py"
macOS / Linux:
mkdir -p ~/.claude/hooks cp statusline.py ~/.claude/statusline.py cp hooks/notify.py ~/.claude/hooks/notify.py
-
(Windows only) Register the toast AppId:
.\scripts\register-app-id.ps1
-
Merge
settings.example.jsoninto~/.claude/settings.json(the included helper does a deep-merge with backup, on any OS):python scripts/merge_settings.py settings.example.json
-
Restart any open Claude Code window.
Verify
From the repo root (no install required):
python tests/test_statusline.py # state matrix; should print "9/9 passed"
python tests/test_watcher.py # watcher logic; should print "7/7 passed"
python tests/test_notify.py # fires 3 toasts + 3 sounds (manual confirm)
python tests/test_colors.py # renders the dot/WAIT states
Then open a Claude Code window and confirm:
- Green
●appears between project name and model while Claude is responding. - Dot disappears within ~1s after the response finishes.
See tests/CHECKLIST.md for cross-window scenarios.
Environment variables
| Variable | Effect |
|---|---|
CLAUDE_QUIET=1 |
Silence the notification hook (no sounds, no toasts). |
CLAUDE_STATUSLINE_DEBUG=1 |
Dump the raw statusline payload to ~/.claude/statusline-payload.json. |
CLAUDE_SESSIONS_DIR=<path> |
Override the sessions dir read by the statusline + watcher (default ~/.claude/sessions). Useful for tests and isolated environments. |
COLUMNS=N |
Force terminal width (otherwise autodetected via CONOUT$ on Windows). |
Troubleshooting
Toasts don't appear (sounds work). The Anthropic.ClaudeCode AppId isn't registered. Run .\scripts\register-app-id.ps1. After registration, "Claude Code" appears in Settings → System → Notifications → Notifications from apps and other senders — make sure it's toggled on, and that Focus / Do not disturb is off.
Sounds all sound identical. Windows' default sound scheme maps Asterisk / Default Beep / Critical Stop to the same .wav. Customize the events in Sound Control Panel → Sounds if you want distinct audio cues.
Dot never appears. Verify Python is on PATH and the statusLine command in settings.json points to the right path. Run python ~/.claude/statusline.py < ~/.claude/statusline-payload.json (after setting CLAUDE_STATUSLINE_DEBUG=1 once to generate the payload) to see direct output.
Project name truncated even on a wide terminal. Terminal width detection isn't picking up your console. Set COLUMNS=120 (or whatever your width is) in your shell profile.
Known limitations
- Sparse refresh during silent work. Claude Code doesn't fire statusline refreshes on the
refreshIntervaltimer reliably during long thinking turns or long tool calls — repaints fire on state events (start/end of turn, permission prompts). The dot accurately reflects state at the last paint, not in real time. Runwatcher.pyin a separate terminal to get reliable real-time "stuck" detection; it polls on its own schedule and doesn't depend on Claude Code's repaint cadence. - Toast latency on Windows.
hooks/notify.pyspawns a fresh PowerShell per notification (~300ms startup). Fire-and-forget viaPopenso it doesn't block, but the toast itself appears ~300ms after the event. A persistent helper would be cleaner — future work.
Design notes
- Statusline reads stdin payload Claude Code provides on every render: session id, transcript path, cwd, model, context window, rate limits.
- Busy state comes from
~/.claude/sessions/<PID>.json— Claude Code'sstatusfield there is the authoritative liveness signal (idle/busy/waiting). - Last activity is the timestamp of the last non-thinking JSONL entry, rendered as local wall-clock time. Extended-thinking blocks land in the JSONL and update file mtime, so naive
getmtimewould hide a session that's been silently reasoning for minutes. The statusline (andwatcher.py's classification, by the same logic) walks the tail of the JSONL backwards, skipssubtype=="thinking"entries, and uses the previous entry's timestamp. Falls back to file mtime when no parseable non-thinking entries exist. Color buckets (dim/yellow/red) still classify freshness at the last paint. - Toast AppId is registered in
HKCU:\SOFTWARE\Classes\AppUserModelId\Anthropic.ClaudeCode— Microsoft's documented way to register a standalone notifier without needing a Start Menu shortcut. - Notification backends are platform-detected.
hooks/notify.pydefines aNotificationBackendABC withWindowsBackend/MacOSBackend/LinuxBackendimplementations selected viasys.platform. Adding a new platform is one new class. - The watcher polls externally, free of Claude Code's repaint quirks. It scans
~/.claude/sessions/*.jsonplus the matching transcript JSONLs in~/.claude/projects/<encoded-cwd>/, classifies each session, and beeps when severity escalates (debounced — won't beep continuously while a session sits in WAIT).
License
MIT — see LICENSE.
Metadata
Release files for claude-watcher 0.4.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| claude_watcher-0.4.2.tar.gz | 45.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| claude_watcher-0.4.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 79.1 kB
Release files / claude_watcher-0.4.2.tar.gz
| Download URL | claude_watcher-0.4.2.tar.gz |
|---|---|
| Size | 45.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
951a5da885013b83153b948c431061dba3d700eaca3e4f5f97b3a93daee8da14
|
|
BLAKE2b-256 checksum How to use checksums |
7ddf0fab587ee48aec766f7aae4b7eb7077fe3bdbe62ab4d4604e93e6ced910e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 23, 2026.
Transparency logRelease files / claude_watcher-0.4.2-py3-none-any.whl
| Download URL | claude_watcher-0.4.2-py3-none-any.whl |
|---|---|
| Size | 33.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a0356a2eb76140d43c11bcdebdaf0627dd0aa4e40bfcd2084fd31c51988cd092
|
|
BLAKE2b-256 checksum How to use checksums |
8dff670c7ffbce96910d28a565149d270f266f70162f737722452d7299ceadf5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 23, 2026.
Transparency log