A multi-line status line for Claude Code, plus a per-row renderer for the
subagent panel. Pure bash, one hard dependency (jq), and a single
self-contained installer you can hand to a teammate.
Every segment is dropped when its data is absent, so the same script degrades cleanly across versions, plans and subscription types.
curl -fsSL https://raw.githubusercontent.com/ProcessPal/claude-statusline/main/install-claude-statusline.sh -o install-claude-statusline.sh
bash install-claude-statusline.shNothing else is needed — both scripts are embedded in that one file, so it also works fine over Slack, email or a USB stick.
./install-claude-statusline.sh # install or upgrade
./install-claude-statusline.sh --dry-run # show changes, write nothing
./install-claude-statusline.sh --uninstall # revert settings, keep scripts
./install-claude-statusline.sh --help # all flags| Flag | Effect |
|---|---|
--config-dir DIR |
Install into DIR (default $CLAUDE_CONFIG_DIR, else ~/.claude) |
--refresh-interval N |
Re-run every N seconds (default 10, 0 disables) |
--padding N |
Horizontal padding (default: unset) |
--no-subagent |
Skip the subagent row renderer |
--force |
Overwrite scripts that have local edits |
The installer backs up settings.json before touching it, refuses to run
against invalid JSON, preserves your other settings keys, keeps local edits to
the scripts unless --force, and finishes with a smoke test so a blank status
line surfaces at install time rather than three sessions later.
Claude Code resolves its config from CLAUDE_CONFIG_DIR, falling back to
~/.claude. Parallel workspaces each get their own config dir and their own
settings.json — a status line installed in one is invisible to the others.
Two ways to cover them.
One canonical script, every workspace points at it. Preferred once you have
more than a couple: install normally, then set each workspace's settings.json
to reference $HOME/.claude explicitly.
Apply to a whole tree of workspaces:
cd ~/.claude/.claudes
for d in */; do
s="$d/config/settings.json"; [ -f "$s" ] || continue
cp "$s" "$s.bak"
jq '.statusLine={type:"command",command:"bash \"$HOME/.claude/statusline-command.sh\"",refreshInterval:10}
| .subagentStatusLine={type:"command",command:"bash \"$HOME/.claude/subagent-statusline.sh\""}
| .hideVimModeIndicator=true' "$s" > "$s.tmp" && mv "$s.tmp" "$s"
doneOne script to update, every workspace follows. If a workspace already holds its
own copies of the scripts, replace them with symlinks rather than deleting —
any ${CLAUDE_CONFIG_DIR}/... reference keeps resolving and cannot drift:
ln -sf "$HOME/.claude/statusline-command.sh" <workspace>/config/statusline-command.sh
ln -sf "$HOME/.claude/subagent-statusline.sh" <workspace>/config/subagent-statusline.shSelf-contained per workspace. Use when a workspace must survive on its own, at the cost of updating each one separately:
./install-claude-statusline.sh --config-dir ~/.claude/.claudes/my-workspace/config--config-dir pins absolute paths in that workspace's settings; a bare install
into ~/.claude uses ${CLAUDE_CONFIG_DIR:-$HOME/.claude} instead, so the same
settings file resolves correctly from anywhere.
Caches namespace themselves per config dir automatically — a digest of the full
path, not its name, since every workspace config dir tends to be called
config. Concurrent workspaces never collide.
- Line 1 — model, effort level, context bar (green → red at 50/75/90/95%), cwd, git branch with unstaged/staged/ahead/behind counts, worktree name.
- Line 2 — plan phase tracker (lowest-numbered
## Phase Nheading that still has unchecked- [ ]items, across the newestplans/<dir>/*.md), version, vim mode, agent, session name, PR number with review state. - Lines 3–4 — 5h/7d rate-limit usage with local reset times, lines changed, session cost, duration.
Beyond the generic drop-when-absent rule, four segments have their own
conditions: rate limits need a Claude.ai Pro/Max plan, the phase line a plans/
directory, max a model that takes an effort parameter, ⑂ a linked worktree
and ⇅ an open PR on the branch.
subagent-statusline.sh replaces the default name · description · token count
row in the agent panel. It receives the whole panel as one JSON object —
columns plus a tasks array — and writes one {"id","content"} line per row
it wants to own.
🔄 scout [Explore] scanning repo for callers ⏱ 1m 35s 🪙 15k 8%
✅ tester [general-purpose] ran suite ⏱ 1h 2m 🪙 1.2M 62%
❌ a-very-long-agent-name-… [code-reviewer] review ⏱ 30s
⏸ idle-one waiting
label wins over name and is trimmed to 24 cells; the description gets
columns - 60. Status drives both emoji and colour across the
running/idle/done/failed/blocked/cancelled vocabularies. Type, description,
duration and tokens are each dropped when absent.
The percentage beside the token count is that row's own context usage —
tokenCount against the task model's contextWindowSize — coloured on the same
50/75/90 thresholds as the main context bar. contextWindowSize arrived in
Claude Code 2.1.205 and is absent until a task's model resolves, so the
percentage drops out rather than reading 0%.
Two affordances from the spec go unused: omitting a task's id keeps Claude
Code's default row for it, and an empty content hides a row outright. This
script emits a row for every task.
- Claude Code 2.x — uses
context_windowandrate_limitsstdin fields bash,jq, plus the POSIX base (awk,sed,find,git)- macOS and Linux. On Windows use Git Bash and forward slashes in the settings path — backslashes get eaten as escape characters.
There is no bc or GNU-coreutils requirement: digests fall back through
shasum → sha1sum → md5sum → cksum, and every stat/date call has a
BSD form with a GNU fallback.
Points where this follows the official statusline docs, since they're easy to get wrong:
- Cache keys use
session_id, never$$. The docs call this out specifically: a pid changes on every invocation and silently defeats the cache. Git state is cached 5s, the plan-phase scan 30s and refreshed in a detached background job. - Caches are namespaced per config dir. Sandbox configs are all named
config, so the namespace is salted with a digest of the full path (config-3581e2cb) rather than the directory name. Legacy fixed-name cache dirs get swept on upgrade. hideVimModeIndicatoris set because this script draws the vim badge itself; without it Claude Code renders-- INSERT --a second time.refreshIntervaldefaults to 10s because the status line carries time-based data (duration, reset countdowns) and git state that background subagents mutate while the main session sits idle. Event-driven updates alone would leave those stale.printf '%b'instead ofecho -e, which the docs recommend for reliable escape handling.- Fails loud, not blank. A non-zero exit or empty stdout blanks the status
line, so a missing
jqprints a message instead of vanishing.
Output is deliberately kept fast: the script runs on a 300ms debounce and an in-flight run is cancelled if a new update arrives.
Two segments are computed but never rendered — they survive from an earlier
layout and are each one line away from working in Assemble output:
token_stats→in:85K out:12K/200K. Note the source fields changed meaning in Claude Code 2.1.132:context_window.total_*_tokensare what's currently in the window, not session totals.ctx_emoji→ the 🟢/🟡/🟠/🔴/🚨 threshold emoji beside the bar
The stdin schema
carries a good deal more that this script ignores: model.id,
workspace.added_dirs, workspace.repo.{host,owner,name},
cost.total_api_duration_ms, context_window.current_usage.* (cache reads and
writes broken out), exceeds_200k_tokens, thinking.enabled, prompt_id,
transcript_path, output_style.name and pr.url.
worktree.* is unused and is not what the ⑂ badge reads. It exists only for
--worktree sessions, whereas workspace.git_worktree — the badge's source —
fires for any linked worktree. They are easy to confuse.
On the subagent side, per-task model is unread: it names the model behind
contextWindowSize, and the size alone is what the row percentage needs.
- Nothing appears — the status line only runs after you accept the
workspace trust prompt.
disableAllHooks: truealso disables it. Runclaude --debugto see the exit code and stderr from the first invocation. - Values are empty — fields are null until the first API response lands.
- No rate-limit lines — those only exist for Claude.ai Pro/Max accounts.
- Garbled output — multi-line output with heavy ANSI is the most
glitch-prone combination; drop
--refresh-intervalto 0 or simplify if your terminal struggles.
Test any change with mock input before trusting it:
echo '{"model":{"display_name":"Opus"},"workspace":{"current_dir":"/tmp"},
"context_window":{"used_percentage":25,"remaining_percentage":75},
"session_id":"test"}' | ./statusline-command.shThe bar needs both used_percentage and remaining_percentage; pass only the
first and it renders nothing, which looks like a fault but isn't.
statusline-command.sh and subagent-statusline.sh are the sources of truth.
After editing either, regenerate the distributable so it can't drift:
./build-installer.sh # tests, syntax-checks, embeds, verifies, stamps the buildThe build runs tests/run.sh first and refuses to embed a failing script.
Run the suite directly while iterating:
./tests/run.sh # everything
./tests/git-indicators.sh # one suiteBoth suites render the real scripts and assert on visible text with ANSI
stripped. git-indicators.sh builds a throwaway repo with an upstream so
ahead/behind actually resolve; each case uses a fresh session_id because the
git cache has a 5s TTL keyed on it. Cases that aren't about elapsed time omit
startTime — pinning a second-scale duration would race the suite's runtime.