The short version
- A Claude Code status line is a script you configure. It receives session data as JSON on stdin and prints a row above the footer. The docs say it runs locally and does not consume API tokens.
- Set it up with the /statusline command, or add a statusLine entry with type command to settings.json.
- The JSON includes context-window percentage, estimated session cost, 5-hour and 7-day plan usage (Pro and Max only) and prompt cache health (v2.1.251 or later).
- Guard every optional field, keep the script fast, and cache slow git calls by session_id. A non-zero exit or empty output blanks the line.
- Session cost is a client-side estimate at list price. On Pro and Max it is not relevant for billing, so use it as a guide.
A Claude Code status line is a row in the terminal that runs a script you choose and shows whatever the script prints. Claude Code passes the script session data as JSON, so you can keep context fill, session cost, plan limits and git state in view without typing a command. Per the docs, it runs locally and does not consume API tokens.
This guide covers setup, the fields worth showing, a working script, and the mistakes that leave the line blank or stale. It is based on the official status line and cost documentation.
What is the Claude Code status line?
The status line renders in its own row above the built-in footer badges. It does not replace them, but once a custom status line is configured, Claude Code stops showing most of the footer's keyboard hints, including esc to interrupt.
Your script reads JSON on stdin and writes text to stdout. Each line you print becomes a row. You can use ANSI colors, and OSC 8 escape sequences for clickable links in terminals that support them (iTerm2, Kitty, WezTerm).
Three reasons to use one:
- Context fill. You see how full the window is before a compaction fires.
- Which session is which. With several sessions open, the line can show the branch, worktree or session name.
- Limits and spend. Plan usage and cost sit next to your prompt instead of behind a command.
How do I set up a status line?
There are two routes.
Option 1: let Claude write it
Run /statusline with a plain-language request. Claude Code generates a script in ~/.claude/ and updates your settings:
/statusline show model name and context percentage with a progress bar
Approve the file edit prompts if Claude Code asks for permission.
Option 2: configure it by hand
Add a statusLine field to ~/.claude/settings.json (user settings) or to project settings. Set type to "command" and point command at a script:
{
"statusLine": {
"type": "command",
"command": "~/.claude/statusline.sh",
"padding": 2
}
}
The command runs in a shell, so an inline command works too. This one uses jq to print the model and context percentage:
{
"statusLine": {
"type": "command",
"command": "jq -r '\"[\\(.model.display_name)] \\(.context_window.used_percentage // 0)% context\"'"
}
}
There is also a refreshInterval setting that re-runs the command on a fixed timer. You need it for clocks, or when the line depends on something that changes while the main session is idle.
A minimal working script
Save this as ~/.claude/statusline.sh:
#!/bin/bash
input=$(cat)
MODEL=$(echo "$input" | jq -r '.model.display_name')
DIR=$(echo "$input" | jq -r '.workspace.current_dir')
PCT=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
echo "[$MODEL] ${DIR##*/} | ${PCT}% context"
Then run chmod +x ~/.claude/statusline.sh. Claude Code reloads settings automatically and runs the script as soon as you save the settings file.
To test without starting a session, pipe in mock input:
echo '{"model":{"display_name":"Opus"},"workspace":{"current_dir":"/home/user/project"},"context_window":{"used_percentage":25},"session_id":"test-session-abc"}' | ~/.claude/statusline.sh
What data can the status line show?
The JSON is larger than most people expect. These fields are the most useful day to day:
| Field | What it gives you |
|---|---|
model.id, model.display_name
|
Current model |
workspace.current_dir |
Where you are |
workspace.git_worktree, worktree.*
|
Linked worktree details; only present inside one |
context_window.used_percentage |
Percent of the context window used |
cost.total_cost_usd |
Estimated session cost |
cost.total_duration_ms, cost.total_api_duration_ms
|
Session time, and time spent waiting on the API |
rate_limits.five_hour, rate_limits.seven_day
|
used_percentage and resets_at for each plan window |
prompt_cache.* |
Cache warmth, hit ratio, misses |
session_name, agent.name, pr.*
|
Session label, active agent, open PR or merge request |
Two details matter for accuracy:
-
used_percentageis calculated from input tokens only (input_tokens + cache_creation_input_tokens + cache_read_input_tokens). Output tokens are not included./contextcan read higher because it adds an estimate for messages added since the last API response. -
cost.total_cost_usdis computed locally from token counts at list price, unless an administrator has set amodelPricingtable in managed settings. The cost docs call it an estimate. They also say that on Pro and Max the session cost figure is not relevant for billing, because usage is included in the subscription. The total resets to $0 when/clearstarts a new session.
Plan limits only appear for some accounts
rate_limits is present only for claude.ai Pro and Max subscribers (or behind a Claude apps gateway with a spend limit), and only after the first API response in a session. Each window can be absent on its own, and Claude Code drops a window once its resets_at time passes. A script that assumes the field exists will misbehave on API-key setups.
Cache health
The prompt_cache object needs Claude Code v2.1.251 or later and appears after the main conversation's first API response. It includes warm, ttl ("5m" or "1h"), expires_at, hit_ratio (0 to 1) and misses. From v2.1.260 it also reports a likely cause for the last miss, such as tools_changed, system_prompt_changed or ttl_expired_5m. The same statistics appear on the Prompt cache (main) line of /usage. Subagent requests are not counted.
A miss means a request re-processed content the cache already held. In the docs' definition, that is more than 5% and at least 2,000 tokens of what could have been read from cache, with no compaction or tool-result clearing to explain it.
A status line for context, cost and limits
This script puts a colored context bar, cost and plan usage on one row, and copes with absent fields:
#!/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)
COST=$(echo "$input" | jq -r '.cost.total_cost_usd // 0')
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')
GREEN='\033[32m'; YELLOW='\033[33m'; RED='\033[31m'; RESET='\033[0m'
if [ "$PCT" -ge 90 ]; then C="$RED"
elif [ "$PCT" -ge 70 ]; then C="$YELLOW"
else C="$GREEN"; fi
FILLED=$((PCT / 10)); EMPTY=$((10 - FILLED))
printf -v FILL "%${FILLED}s"; printf -v PAD "%${EMPTY}s"
BAR="${FILL// /#}${PAD// /-}"
LIMITS=""
[ -n "$FIVE_H" ] && LIMITS=" | 5h $(printf '%.0f' "$FIVE_H")%"
[ -n "$WEEK" ] && LIMITS="$LIMITS 7d $(printf '%.0f' "$WEEK")%"
echo -e "[$MODEL] ${C}${BAR}${RESET} ${PCT}% | $(printf '$%.2f' "$COST")${LIMITS}"
The color thresholds (green under 70%, yellow 70 to 89%, red from 90%) match the docs' multi-line example; adjust them to taste. The // empty idiom prints nothing when a field is missing, so the limits segment disappears for accounts without rate_limits.
How often does the status line run?
It runs once when a session starts or resumes, then again when:
- a new assistant message arrives
-
/compactfinishes - the permission mode changes
- vim mode toggles
- the
commandin your settings changes - a
refreshIntervaltimer elapses, if you set one - a rate-limit window reaches its
resets_attime, or a warm prompt cache reaches itsexpires_attime
Updates are debounced at 300 ms, except that a change to the command itself runs right away. If a new update triggers while your script is still running, Claude Code cancels the in-flight run. The event-driven triggers can go quiet when the main session is idle, for example while a coordinator waits on background subagents. Set refreshInterval if the line must stay current then.
Common problems and fixes
The line is blank. Check these in turn:
- The script is not executable (
chmod +x). - It writes to stderr instead of stdout.
- It exits non-zero or prints nothing. Claude Code displays output only after the script exits with code 0.
- You have not accepted the workspace trust dialog for the folder. Until then the line stays blank, and
claude --debuglogsStatus line command skipped: workspace trust not accepted. -
disableAllHooksistrueoutside managed settings, in which case only astatusLinefrom managed settings runs. Or your organization setsallowManagedHooksOnly, which means you can only get a status line from managed settings.
claude --debug logs your script's stderr on every invocation and its exit code on the first one in a session, which makes it the quickest way to find a parse error.
Values show -- or empty. Fields can be null before the first API response. context_window.used_percentage may be null early in a session, and context_window.current_usage is null again right after /compact until the next API call. Use fallbacks such as // 0.
The line lags. The script runs often, and git status or git diff in a large repo is slow. Cache the result in a temp file for a few seconds. Key the cache on session_id from the JSON, not on $$: a process ID changes on every invocation and defeats the cache, while session_id is stable within a session and unique across concurrent ones. The docs include a full example that refreshes git info every 5 seconds.
Links are not clickable. Terminal.app does not support them. If your terminal does but the link text is not clickable, Claude Code may not have detected hyperlink support; launch with FORCE_HYPERLINK=1 claude. SSH and tmux can strip the escape sequences. If escape codes show up as literal text, use printf '%b' instead of echo -e.
Windows paths. With Git Bash, write paths in command with forward slashes. Unquoted backslashes are treated as escape characters, and the command fails without a visible error.
Garbled output. Complex escape sequences, especially in multi-line status lines, can occasionally corrupt the display. Simplify to plain text if you see it.
Status lines for subagents
A separate subagentStatusLine setting customizes the row shown for each subagent in the agent panel. Its command runs once per refresh tick and receives all visible rows as one JSON object, with a tasks array whose entries include fields such as name, status, model, tokenCount and contextWindowSize. You answer with one JSON line per row, in the form {"id": "<task id>", "content": "<row body>"}. Omit id to keep the default row; send an empty content to hide it. The same trust, disableAllHooks and allowManagedHooksOnly gates apply as for statusLine. If you split work across helpers, this is a cheap way to see which one is using the most tokens.
Should the status line be shared with a team?
Because statusLine executes a shell command, it falls under the same workspace-trust rule as hooks in settings files. A project-level status line therefore runs code from the repository on whoever trusts the folder. Keep shared scripts small and reviewable, and prefer user settings for personal preferences. Plugins can ship a default subagentStatusLine, but unlike hooks, those values do not run under allowManagedHooksOnly.
Conclusion
A status line is a small script that keeps context fill, estimated cost and plan usage in front of you, so you can compact or start fresh before a session gets expensive. Start with /statusline, move to a hand-written script once you know which fields you read, guard every optional field, and cache anything slow. Treat the cost figure as an estimate; for authoritative billing, the cost docs point to the Usage page in the Claude Console.
Sources
Originally published on the Atlas blog.
Top comments (0)