Claude Code Statusline
A Claude Code statusline is the only readout that is always on screen for the two numbers that
decide your bill: how full the context window is and what the session has cost so far. Everything
else, /context, /usage, /cost, requires you to stop and type a command. The statusline is a
shell script you point Claude Code at; it receives a JSON payload on stdin every time something
changes and whatever it prints to stdout becomes the bar at the bottom of the interface.
Set it in ~/.claude/settings.json (or a project’s .claude/settings.json):
{
"statusLine": {
"type": "command",
"command": "~/.claude/statusline.sh"
}
}
A minimal script that prints the model, the current directory’s folder name, and the context window percentage:
#!/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"
Save it, chmod +x ~/.claude/statusline.sh, and Claude Code picks it up on the next settings
reload, no restart needed. You do not have to write this by hand: /statusline show model name and context percentage with a progress bar asks Claude Code to generate and wire up the script for
you. Full reference: Customize your status line.
The input payload
Claude Code sends one JSON object on stdin per invocation. These are the fields worth building around; consult the primary doc for the complete list.
| Field | Type | Meaning |
|---|---|---|
model.id, model.display_name | string | Current model identifier and display name |
cwd, workspace.current_dir | string | Current working directory (same value, two names) |
workspace.project_dir | string | Directory Claude Code was launched from |
workspace.git_worktree | string | Worktree name, present when cwd is inside a linked git worktree |
cost.total_cost_usd | number | Estimated session cost in USD, computed client-side at list price. Resets to zero on /clear |
cost.total_duration_ms | number | Wall-clock time since the session started, in milliseconds |
cost.total_api_duration_ms | number | Time spent waiting on API responses, in milliseconds |
cost.total_lines_added, cost.total_lines_removed | number | Lines of code changed this session |
context_window.used_percentage | number or null | Pre-calculated percent of the context window used |
context_window.remaining_percentage | number or null | Pre-calculated percent remaining |
context_window.context_window_size | number | Max tokens for the model: 200000 by default, 1000000 for extended-context models |
context_window.current_usage | object or null | Last API response’s input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens. Null before the first call and again right after /compact |
prompt_cache.warm | boolean | Whether the cached prefix is still inside its TTL |
prompt_cache.hit_ratio | number or null | Cache read tokens as a fraction of all input tokens this session |
prompt_cache.ttl | string | "5m" or "1h", the cache lifetime in effect |
prompt_cache.expires_at | number or null | Epoch seconds when the cached prefix goes cold |
session_id | string | Stable per-session identifier, safe to use as a cache-file key |
worktree.name, worktree.branch | string | Present only while the session is running inside a worktree session |
prompt_cache requires Claude Code v2.1.251 or later and only appears after the main
conversation’s first API response; it does not cover subagent requests. context_window.used_percentage
is calculated from input tokens only (input_tokens + cache_creation_input_tokens + cache_read_input_tokens), so it excludes output_tokens if you’re computing it by hand from
current_usage. Source: Available data.
Recipes
Context percentage, so you see compaction coming. This is the number to watch: once a session fills its window, Claude Code has to compact, which is itself a large request. See Claude Code token usage for why the same conversation gets re-billed every turn.
PCT=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
echo "ctx: ${PCT}%"
Session cost.
COST=$(echo "$input" | jq -r '.cost.total_cost_usd // 0')
printf 'cost: $%.2f\n' "$COST"
Cache warmth. A cold cache is the thing to watch right after a break: the prompt cache lives
for 1h on a subscription (5m once you’re drawing on usage credits), and once it expires the
next request re-sends and re-caches the whole prefix at full price instead of a cache-read rate.
WARM=$(echo "$input" | jq -r '.prompt_cache.warm // false')
RATIO=$(echo "$input" | jq -r '.prompt_cache.hit_ratio // 0')
if [ "$WARM" = "true" ]; then
printf 'cache: warm %.0f%%\n' "$(echo "$RATIO * 100" | bc)"
else
echo "cache: cold"
fi
Git branch and worktree name. This starts mattering the moment more than one agent is touching the repo, because each worktree is a separate checkout on its own branch and it is easy to lose track of which terminal is which.
BRANCH=$(git branch --show-current 2>/dev/null)
WT=$(echo "$input" | jq -r '.worktree.name // empty')
[ -n "$WT" ] && echo "worktree: $WT ($BRANCH)" || echo "branch: $BRANCH"
Gotchas
The script runs on every render, so it has to be fast and it must never block. Claude Code cancels
an in-flight script if a new update arrives before it finishes, and a slow script just leaves the
statusline stale until it returns. Anything slow or network-bound (a remote API call, a large git status in a big repo) belongs in a cached file your script reads, keyed on session_id since a
process id changes on every invocation and defeats the cache.
A script that errors does not announce itself: a non-zero exit or empty stdout just makes the statusline go blank. Test by piping a sample payload in by hand before you trust it:
echo '{"model":{"display_name":"Opus"},"workspace":{"current_dir":"/home/user/project"},"context_window":{"used_percentage":25},"session_id":"test-session-abc"}' | ./statusline.sh
Chaining instead of replacing
Most write-ups on this topic assume you’re starting from nothing. In practice you likely already
have a statusline, maybe from ccusage or a starter template, and a new script that silently
overwrites statusLine.command throws that away. The fix is to have your script call the old
command and append its own segment rather than replace it:
#!/bin/bash
input=$(cat)
# Call whatever the previous statusline command was, feeding it the same input,
# then add a segment after it.
PREVIOUS=$(echo "$input" | ~/.claude/old-statusline.sh)
PCT=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
echo "$PREVIOUS | ctx: ${PCT}%"
Point statusLine.command at this wrapper instead of either script alone, and keep the original
script file in place so the wrapper has something to call.
shiploop’s statusline segment
shiploop, a Claude Code harness for running fleets of headless worker agents, ships
/shiploop:statusline for anyone running its governor loop. Each unit of work it dispatches becomes
a ticket, shiploop’s own term for it, the same word you’ll see in its queue file and config. It adds
one segment showing the live worker count, how many tickets the run has answered so far, and the
oldest live worker’s ticket and elapsed time, and it stays silent when no governor run is active. It chains rather than replaces:
installing it records whatever statusLine command was already in ~/.claude/settings.json and
wraps it, the same pattern as above, rather than overwriting it. It is explicitly opt-in, nothing in
scaffold or update ever installs it automatically, only running /shiploop:statusline does. The
refresh interval is controlled by the GOVERN_STATUSLINE_REFRESH environment variable, default
5 seconds.
Last updated 2026-09-04.