Claude Code Status Line
Guide for designing, configuring, and debugging the Claude Code status line — a customizable bar rendered at the bottom of the UI that runs a user-supplied shell script on each session update.
Per core:anti-fabrication: test status-line scripts against the real stdin payload (see Testing Locally) before claiming a field renders — never assert output you have not observed.
What Is the Status Line?
The status line is a shell command that Claude Code executes locally on each session update. Claude Code pipes a JSON payload (model, workspace, context-window, cost, etc.) to the script's stdin and renders the script's stdout verbatim as the bar at the bottom of the UI. It runs on the user's machine, consumes no API tokens, and is temporarily hidden during autocomplete, help menus, and permission prompts.
The status line is gated by the same workspace-trust acceptance as hooks. Setting disableAllHooks: true in settings also disables the status line.
Configuration
Add a statusLine block to ~/.claude/settings.json (user) or .claude/settings.json (project):
{
"statusLine": {
"type": "command",
"command": "~/.claude/statusline.sh",
"padding": 2,
"refreshInterval": 5
}
}
Fields:
type— must be"command"command— path to a script, or an inline shell commandpadding(optional) — extra horizontal spacing in characters (default0)refreshInterval(optional, seconds, min1) — re-runs on a fixed timer in addition to event-driven updates; set this when displaying time-based data or when state changes during idle (e.g. subagent progress). Omit for event-only updates.
The /statusline slash command
Claude Code ships a built-in /statusline <description> command that generates a script and updates settings for you (e.g. /statusline show model name and context percentage with a progress bar). Use /statusline delete to remove the configuration.
Prebuilt status-line tools
Writing a script gives full control with zero dependencies. For a turnkey bar without scripting, community tools render the same stdin payload. The most widely used is ccstatusline (sirmalloc/ccstatusline, MIT) — a Node CLI with an interactive config TUI, powerline themes, gradients, Nerd-Font icons, and widgets for token speed, git PR status, block timers, and more. It needs no install and wires itself into settings.json:
{ "statusLine": { "type": "command", "command": "npx -y ccstatusline@latest" } }
It requires Node/Bun on PATH and runs npx on each update (slower cold-start than a local script). The rest of this skill covers the hand-rolled path, which stays the right choice when you want no runtime dependency, deterministic latency, or behavior the tool does not expose.
Stdin JSON Payload
Every invocation receives a JSON object on stdin containing session metadata. Key top-level fields:
| Need | Field path |
|---|---|
| Model label | model.display_name (human) or model.id (stable) |
| Current folder | workspace.current_dir (basename for display) |
| Project root | workspace.project_dir |
| Context % | context_window.used_percentage (null-guard with // 0) |
| Cumulative tokens | context_window.total_input_tokens, .total_output_tokens |
| Cost USD | cost.total_cost_usd |
| Wall time ms | cost.total_duration_ms |
| Lines changed | cost.total_lines_added, .total_lines_removed |
| Cache key | session_id |
| Transcript file | transcript_path |
| Rate limits | rate_limits.five_hour.used_percentage (Pro/Max only) |
| Output style | output_style.name |
| Vim mode | vim.mode (absent when vim off) |
| Worktree | worktree.* / workspace.git_worktree |
See references/input-schema.md for the full JSON schema, nullable fields, and per-field semantics.
Note on context_window.used_percentage: pre-calculated by Claude Code as (input + cache_creation + cache_read) / context_window_size — output tokens are not included. Use this field rather than computing from current_usage or total_input_tokens (which is cumulative and can exceed the window size).
Default Example
The default script shipped in assets/statusline-default.sh surfaces model, folder, context %, and git branch:
#!/usr/bin/env bash
input=$(cat)
MODEL=$(jq -r '.model.display_name // "Claude"' <<<"$input")
DIR=$(jq -r '.workspace.current_dir // .cwd' <<<"$input")
PCT=$(jq -r '.context_window.used_percentage // 0' <<<"$input" | cut -d. -f1)
BRANCH=""
if git -C "$DIR" rev-parse --git-dir >/dev/null 2>&1; then
B=$(git -C "$DIR" branch --show-current 2>/dev/null)
[ -n "$B" ] && BRANCH=" | 🌿 $B"
fi
echo "[$MODEL] 📁 ${DIR##*/} | ${PCT}% ctx${BRANCH}"
Install it:
cp assets/statusline-default.sh ~/.claude/statusline.sh
chmod +x ~/.claude/statusline.sh
Then add to ~/.claude/settings.json:
{ "statusLine": { "type": "command", "command": "~/.claude/statusline.sh" } }
Renders e.g. [Opus] 📁 my-project | 42% ctx | 🌿 main.
Output Rules
- Stdout is rendered verbatim; each
echo/printline becomes a separate row (multi-line supported). - ANSI color escape codes work:
\033[32mgreen,\033[33myellow,\033[31mred,\033[0mreset. - OSC 8 sequences produce clickable hyperlinks in supporting terminals (iTerm2, Kitty, WezTerm). Set
FORCE_HYPERLINK=1for terminals that aren't auto-detected. - Keep lines short — long output truncates or wraps in narrow terminals.
- Write to stdout only. A non-zero exit code or empty stdout blanks the row. Writes to stderr are not rendered.
Colored context-bar example
PCT=$(jq -r '.context_window.used_percentage // 0' <<<"$input" | cut -d. -f1)
if [ "$PCT" -lt 70 ]; then COLOR="\033[32m" # green
elif [ "$PCT" -lt 90 ]; then COLOR="\033[33m" # yellow
else COLOR="\033[31m"; fi # red
printf "${COLOR}%d%% ctx\033[0m\n" "$PCT"
Update Cadence
- The script runs after each new assistant message, on permission-mode change, and on vim-mode toggle.
- Events are debounced at 300ms.
- If a new trigger fires while a previous run is still executing, the in-flight run is cancelled.
- Edits to the script apply on the next trigger, not retroactively.
- During idle (e.g. waiting on subagents) events stop firing — set
refreshIntervalto keep time-based data live.
Caching & Performance
Expensive operations (git branch lookups, network calls, file reads) should be cached because the script runs on every update.
- Key the cache on
session_id— stable per session. Do not use$$/ pid; those change on every invocation. - Cache under
/tmpor$XDG_CACHE_HOMEwith the session id in the filename. - Skip the cache on miss by returning fast-path defaults; populate it in the background when safe.
SID=$(jq -r '.session_id' <<<"$input")
CACHE="/tmp/statusline-${SID}.branch"
if [ ! -f "$CACHE" ] || [ $(($(date +%s) - $(stat -f %m "$CACHE" 2>/dev/null || stat -c %Y "$CACHE"))) -gt 30 ]; then
git -C "$DIR" branch --show-current 2>/dev/null > "$CACHE"
fi
BRANCH=$(cat "$CACHE" 2>/dev/null)
Subagent Status Line
A parallel subagentStatusLine setting customizes the row rendered inside the subagent panel. Input adds a tasks array with {id, name, tokenCount, ...}. Output expects NDJSON — one JSON object per line:
{"id":"task-abc","content":"analyzing schema..."}
{"id":"task-def","content":"✓ done"}
Use this to project per-subagent progress into the UI.
Testing Locally
Pipe canned JSON to the script:
echo '{
"model": {"display_name": "Opus"},
"workspace": {"current_dir": "/tmp/x"},
"context_window": {"used_percentage": 42},
"session_id": "test"
}' | ~/.claude/statusline.sh
Iterate until the output looks right, then trigger an assistant message in Claude Code to see it rendered.
Common Pitfalls
used_percentageis null before the first API call — always// 0or similar fallback.total_input_tokensis cumulative across the session and can exceed the window size. Never divide it bycontext_window_sizefor a percentage — useused_percentage.rate_limitsis Pro/Max only and absent before the first API response.- Non-zero exit or stderr writes blank the line. Redirect diagnostics:
git ... 2>/dev/null. - Nullable / absent fields:
session_name,workspace.git_worktree,vim,agent,worktree,rate_limits. Default them with// "fallback"injq. - Script edits apply on the next trigger, not instantly — send a message to re-run.
- Windows runs scripts via Git Bash. Invoke PowerShell explicitly:
powershell -NoProfile -File %USERPROFILE%\.claude\statusline.ps1. disableAllHooks: truealso disables the status line.- Autocomplete / help menus / permission prompts hide the status line temporarily — this is expected behavior, not a script failure.