The Claude Code statusline is a statusLine field in settings.json that points to a shell script. Claude Code pipes JSON session data to that script on stdin — model, cost, context window, git branch, rate limits — and prints whatever the script returns to stdout, permanently, at the bottom of the terminal.
That’s the whole mechanism. Everything else — the progress bars, the colored git indicators, the clickable repo links people share screenshots of — is just a script formatting that JSON. The hard part isn’t the shell scripting, it’s knowing which of the ~25 available fields exist, which ones show up as null early in a session, and which ones are gated behind a specific Claude Code version. Most guides cover five fields (model, cost, context, branch, directory) and stop. This one covers all of them, plus the newer subagentStatusLine setting almost nothing has documented yet.
Statusline vs. output style: don’t confuse these
Both live under /config, both are easy to conflate, and they do unrelated jobs:
| Statusline | Output style | |
|---|---|---|
| What it changes | A persistent bar rendered below the conversation | Claude’s system prompt — tone, format, behavior |
| Input | JSON session data on stdin | Nothing — it’s prompt instructions |
| Output | Text/ANSI printed to stdout | Claude’s responses themselves |
| Consumes tokens | No — runs locally | No, but changes what Claude generates |
| Command | /statusline | /output-style |
If you want Claude to explain its reasoning more, that’s an output style. If you want to see context usage without asking, that’s the statusline.
Set it up
Fastest path — describe what you want in plain language and let Claude Code write the script:
/statusline show model name and context percentage with a progress bar
Claude Code writes a script to ~/.claude/ and wires it into your settings automatically. Approve the file-edit prompt if it asks.
Manual path — add a statusLine object to ~/.claude/settings.json (user-level) or .claude/settings.json (project-level):
{
"statusLine": {
"type": "command",
"command": "~/.claude/statusline.sh",
"padding": 2
}
}
command runs in a shell, so an inline one-liner works too — no script file required:
{
"statusLine": {
"type": "command",
"command": "jq -r '\"[\\(.model.display_name)] \\(.context_window.used_percentage // 0)% context\"'"
}
}
To remove it: /statusline delete (or clear / remove it), or delete the statusLine key by hand.
How it actually updates
Your script runs once when a session starts (including on resume), then re-runs on:
- A new assistant message
/compactfinishing- A permission-mode change
- Vim mode toggling
- A
refreshIntervaltimer, if you set one
Updates are debounced at 300ms — rapid changes batch into one re-run. If a new trigger fires while your script is still running, Claude Code kills the in-flight run rather than queuing it. The bar goes quiet during idle periods (e.g. a coordinator session waiting on background subagents); set refreshInterval (minimum 1 second) if you need time-based fields like a clock to keep moving.
Two version-gated behaviors worth knowing if your statusline looks wrong on an older build:
- Before v2.1.216, resuming a session ran the statusline command twice in quick succession, so the first render could flicker before the real one replaced it.
COLUMNS/LINESenvironment variables are only set on your script starting in v2.1.153. Claude Code captures your script’s stdout instead of connecting it to the real terminal, sotput colsdoesn’t work inside the script — read$COLUMNS/$LINESinstead, and only on 2.1.153+.
Every field Claude Code sends you
This is the part most guides skip past. Claude Code sends a JSON object on stdin with the following top-level fields:
| Field | Notes |
|---|---|
model.id, model.display_name | Current model |
cwd, workspace.current_dir | Same value; prefer workspace.current_dir |
workspace.project_dir | Where Claude Code was launched — can differ from cwd |
workspace.added_dirs | Dirs added via /add-dir; empty array if none |
workspace.git_worktree | Worktree name, only inside a linked git worktree add checkout |
workspace.repo.host/owner/name | Parsed from the origin remote; absent without one |
cost.total_cost_usd | Session cost estimate; resets on /clear |
cost.total_duration_ms, cost.total_api_duration_ms | Wall-clock vs. time-waiting-on-API |
cost.total_lines_added, cost.total_lines_removed | Lines changed this session |
context_window.used_percentage / remaining_percentage | Pre-calculated; input-tokens-only formula |
context_window.context_window_size | 200000 by default, 1000000 for extended-context models |
context_window.current_usage.* | Per-category breakdown (input/output/cache read/cache write); null before the first API call and again right after /compact |
exceeds_200k_tokens | Fixed 200k threshold, independent of the model’s actual window size |
fast_mode | Whether fast mode is on |
effort.level | low / medium / high / xhigh / max; absent if the model doesn’t support the effort parameter (Ultracode reports as xhigh) |
thinking.enabled | Extended thinking on/off |
rate_limits.five_hour / .seven_day | used_percentage + resets_at; Pro/Max subscribers only, after the first API response |
session_id, session_name | session_name only appears once a custom name (--name//rename) or an AI-generated title exists |
prompt_id | UUID of the in-flight prompt; requires v2.1.196+, absent until first input |
transcript_path, version | File path and Claude Code version |
output_style.name | Active output style |
vim.mode | NORMAL/INSERT/VISUAL/VISUAL LINE, only when vim mode is on |
agent.name | Set when running with --agent or agent settings |
pr.number, pr.url, pr.review_state | Open PR for the current branch; disappears once merged/closed |
worktree.name/path/branch/original_cwd/original_branch | Present only during --worktree sessions |
A handful of fields are null rather than absent early in a session — context_window.current_usage and the two used_percentage/remaining_percentage fields specifically. Handle both cases: absent keys need a fallback (// empty in jq), null values need a default (// 0).
Full example JSON payload
{
"cwd": "/current/working/directory",
"session_id": "abc123...",
"session_name": "my-session",
"prompt_id": "550e8400-e29b-41d4-a716-446655440000",
"transcript_path": "/path/to/transcript.jsonl",
"model": { "id": "claude-opus-5", "display_name": "Opus" },
"workspace": {
"current_dir": "/current/working/directory",
"project_dir": "/original/project/directory",
"added_dirs": [],
"git_worktree": "feature-xyz",
"repo": { "host": "github.com", "owner": "anthropics", "name": "claude-code" }
},
"version": "2.1.224",
"output_style": { "name": "default" },
"cost": {
"total_cost_usd": 0.01234,
"total_duration_ms": 45000,
"total_api_duration_ms": 2300,
"total_lines_added": 156,
"total_lines_removed": 23
},
"context_window": {
"total_input_tokens": 15500,
"total_output_tokens": 1200,
"context_window_size": 200000,
"used_percentage": 8,
"remaining_percentage": 92,
"current_usage": {
"input_tokens": 8500,
"output_tokens": 1200,
"cache_creation_input_tokens": 5000,
"cache_read_input_tokens": 2000
}
},
"exceeds_200k_tokens": false,
"fast_mode": false,
"effort": { "level": "high" },
"thinking": { "enabled": true },
"rate_limits": {
"five_hour": { "used_percentage": 23.5, "resets_at": 1738425600 },
"seven_day": { "used_percentage": 41.2, "resets_at": 1738857600 }
},
"vim": { "mode": "NORMAL" },
"agent": { "name": "security-reviewer" },
"pr": { "number": 1234, "url": "https://github.com/anthropics/claude-code/pull/1234", "review_state": "pending" },
"worktree": {
"name": "my-feature",
"path": "/path/to/.claude/worktrees/my-feature",
"branch": "worktree-my-feature",
"original_cwd": "/path/to/project",
"original_branch": "main"
}
}
subagentStatusLine: a per-agent row, not a bar
Separate setting, same shape, different target. subagentStatusLine replaces the default name · description · token count row that renders for each subagent in the agent panel below the prompt:
{
"subagentStatusLine": {
"type": "command",
"command": "~/.claude/subagent-statusline.sh"
}
}
The mechanics differ from the main statusline in ways worth knowing before you build one:
- It runs once per refresh tick for all visible rows combined — not once per subagent. The script receives a single JSON object with a
columnsfield (usable row width) and atasksarray, one entry per subagent row currently shown. - Each task carries
id,name,type,status,description,label,startTime,model,effort,contextWindowSize,tokenCount,tokenSamples,cwd. modelandcontextWindowSizerequire v2.1.205+ and are omitted until the subagent’s model resolves.effortrequires v2.1.214+ — it reports the value as configured in the subagent’s frontmatter or invocation, not necessarily what the model actually applies if it doesn’t support that level.- Output is one JSON line per row you want to override:
{"id": "<task id>", "content": "<row body>"}. Omit a task’sidto keep the default row; emit an emptycontentto hide it entirely. ANSI colors and OSC 8 links work incontent.
This is the piece that’s actually useful for people running several agents at once, and it’s what almost every third-party statusline write-up skips, because it postdates most of them. If you run parallel agents across git worktrees, pairing subagentStatusLine’s per-task contextWindowSize/tokenCount with the main statusline’s workspace.git_worktree field gets you a dashboard that shows, at a glance, which worktree is which agent and how close each one is to running out of context — something no single built-in view currently shows.
Ready-to-use examples
Save any of these to ~/.claude/statusline.sh, chmod +x it, then point command at the path.
Context usage bar (model + a 10-block progress bar):
#!/bin/bash
input=$(cat)
MODEL=$(echo "$input" | jq -r '.model.display_name')
PCT=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
BAR_WIDTH=10
FILLED=$((PCT * BAR_WIDTH / 100))
EMPTY=$((BAR_WIDTH - FILLED))
BAR=""
[ "$FILLED" -gt 0 ] && printf -v FILL "%${FILLED}s" && BAR="${FILL// /▓}"
[ "$EMPTY" -gt 0 ] && printf -v PAD "%${EMPTY}s" && BAR="${BAR}${PAD// /░}"
echo "[$MODEL] $BAR $PCT%"
Rate limits (handles the field being absent for API-key users):
#!/bin/bash
input=$(cat)
MODEL=$(echo "$input" | jq -r '.model.display_name')
FIVE_H=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty')
WEEK=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty')
LIMITS=""
[ -n "$FIVE_H" ] && LIMITS="5h: $(printf '%.0f' "$FIVE_H")%"
[ -n "$WEEK" ] && LIMITS="${LIMITS:+$LIMITS }7d: $(printf '%.0f' "$WEEK")%"
[ -n "$LIMITS" ] && echo "[$MODEL] | $LIMITS" || echo "[$MODEL]"
Caching slow git calls — your script runs on every assistant message, so git status/git diff in a large repo adds visible lag. Cache to a temp file keyed on session_id (not $$/PID, which changes every invocation and defeats the cache):
#!/bin/bash
input=$(cat)
SESSION_ID=$(echo "$input" | jq -r '.session_id')
CACHE_FILE="/tmp/statusline-git-cache-$SESSION_ID"
CACHE_MAX_AGE=5
cache_is_stale() {
[ ! -f "$CACHE_FILE" ] || \
[ $(($(date +%s) - $(stat -c %Y "$CACHE_FILE" 2>/dev/null || stat -f %m "$CACHE_FILE" 2>/dev/null || echo 0))) -gt $CACHE_MAX_AGE ]
}
if cache_is_stale; then
BRANCH=$(git branch --show-current 2>/dev/null)
echo "$BRANCH" > "$CACHE_FILE"
fi
echo "🌿 $(cat "$CACHE_FILE")"
The full set of official examples — colored git status, cost/duration tracking, multi-line layouts, clickable OSC 8 links, Windows PowerShell/Git Bash variants — is in Anthropic’s statusline docs.
Which community tool should you actually install
If writing your own script isn’t the point, several maintained options exist. What we found comparing them:
| Tool | What it’s for | Notable |
|---|---|---|
sirmalloc/ccstatusline | Themeable, config-driven, no scripting required | Referenced directly in Anthropic’s own docs as a starting point |
martinemde/starship-claude | Starship-prompt-style theming for Claude Code | Good fit if you already use Starship in your shell |
ilia-pluzhnikov/claude-code-statusline | Dependency-free Node.js, single file | Shows model, git state, context burned, cache state, subscription rate limits |
haunchen/claude-code-statusline | Peak/off-peak-hour aware | Adjusts what it shows based on Anthropic’s peak usage windows |
kcchien/claude-code-statusline | Gradient progress bar, smart hiding | Denser visual style; hides segments when there’s nothing to show |
All five read the same JSON schema documented above — the differences are entirely in presentation and configuration ergonomics, not in what data is available to them.
Troubleshooting
Nothing shows up
Confirm the script is executable (chmod +x), writes to stdout (not stderr), and produces output when you pipe mock JSON into it directly. If disableAllHooks is true in settings, the statusline is disabled too — it runs under the same trust gate as hooks. Run claude --debug to see the exit code and stderr from the first invocation.
Fields show blank or --
Several fields are legitimately null before the first API response (context_window.current_usage, the two percentage fields). Add fallbacks (// 0 in jq, or 0 in Python). If values stay empty after several messages, restart the session.
Status line stays blank even though the script looks right
Check workspace trust. statusLine executes a shell command, so it needs the same trust acceptance as hooks — if you haven’t accepted the trust dialog for the folder, claude --debug logs Status line command skipped: workspace trust not accepted.
OSC 8 links render but aren’t clickable
Needs a terminal that supports OSC 8 (iTerm2, Kitty, WezTerm — not Terminal.app). If Claude Code fails to auto-detect support (common on Windows Terminal), force it: FORCE_HYPERLINK=1 claude.
Windows paths break the command
Git Bash treats unescaped backslashes as escape characters, so C:\Users\...\script.mjs silently loses its separators. Use forward slashes in the command string, or invoke PowerShell explicitly: powershell -NoProfile -File C:/Users/you/.claude/statusline.ps1.
FAQ
Does the statusline cost API tokens? No. It runs locally as a shell process; Claude Code never sends its output back to the model.
Can I show different information per project?
Yes — put a statusLine object in .claude/settings.json at the project level. It’s checked in the same precedence chain as any other setting: managed > CLI flags > local > project > user.
Why is context_window.used_percentage different from what /context shows?
They’re calculated at different moments in the request lifecycle. used_percentage reflects the most recent API response; /context recalculates on demand. Small discrepancies right after a message are expected.
What’s the difference between statusLine and subagentStatusLine?
statusLine controls the single bar at the bottom of the terminal for the main session. subagentStatusLine controls the per-row text for each subagent listed in the agent panel — a completely separate render target with its own JSON shape (a tasks array, not a flat session object).
Do I need jq installed?
Only for the Bash examples. Python and Node.js scripts can parse the JSON with their standard libraries (json.load(sys.stdin) / JSON.parse), no dependency required.
Can a slow statusline script hang Claude Code?
It blocks the statusline from updating, not the rest of the session — but if you’re chaining git calls in a large repo, cache them (see the caching example above), since the script re-runs on every assistant message.
Browse how real projects configure settings.json, hooks, and permissions in our rules gallery.