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.

FieldTypeMeaning
model.id, model.display_namestringCurrent model identifier and display name
cwd, workspace.current_dirstringCurrent working directory (same value, two names)
workspace.project_dirstringDirectory Claude Code was launched from
workspace.git_worktreestringWorktree name, present when cwd is inside a linked git worktree
cost.total_cost_usdnumberEstimated session cost in USD, computed client-side at list price. Resets to zero on /clear
cost.total_duration_msnumberWall-clock time since the session started, in milliseconds
cost.total_api_duration_msnumberTime spent waiting on API responses, in milliseconds
cost.total_lines_added, cost.total_lines_removednumberLines of code changed this session
context_window.used_percentagenumber or nullPre-calculated percent of the context window used
context_window.remaining_percentagenumber or nullPre-calculated percent remaining
context_window.context_window_sizenumberMax tokens for the model: 200000 by default, 1000000 for extended-context models
context_window.current_usageobject or nullLast 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.warmbooleanWhether the cached prefix is still inside its TTL
prompt_cache.hit_rationumber or nullCache read tokens as a fraction of all input tokens this session
prompt_cache.ttlstring"5m" or "1h", the cache lifetime in effect
prompt_cache.expires_atnumber or nullEpoch seconds when the cached prefix goes cold
session_idstringStable per-session identifier, safe to use as a cache-file key
worktree.name, worktree.branchstringPresent 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.