ClaudeMap

·Skills & Commands

A 2026 guide to the Claude Code statusline — the statusLine setting, the full stdin JSON protocol (model, context_window, cost, rate_limits), three copy-paste bash templates, event-driven refresh timing, and the five fastest fixes for a blank status line.

Claude Code Statusline: The stdin JSON Protocol and Reusable Script Templates (2026)

The Claude Code statusline is a customizable row at the bottom of the terminal, rendered by an external command script — Claude Code pipes the session state (model, directory, context usage, cost, rate limits) to the script's stdin as JSON, and whatever the script prints to stdout becomes the status line. Based on the official documentation as of September 2026 (v2.1.251), this guide covers the statusLine setting, the full stdin JSON protocol, three copy-paste script templates, and the five fastest fixes for a blank status line.

TL;DR

  • Add statusLine: { type: "command", command: "..." } to ~/.claude/settings.json, or generate one with the /statusline command
  • Your script receives JSON on stdin: model / workspace / context_window / cost / rate_limits are all in there
  • Updates are event-driven (new messages, /compact, permission-mode switches); refreshInterval adds timed refreshes
  • Output must go to stdout; many fields can be missing or null — always fall back with // 0
  • Don't want to write a script? ccstatusline (12.7k stars, September 2026) and friends already exist

Five-minute setup

The status line is configured in settings.json (user-level ~/.claude/settings.json or project-level):

{
  "statusLine": {
    "type": "command",
    "command": "~/.claude/statusline.sh",
    "padding": 2
  }
}

The three fields:

| Field | Required | Meaning | |---|---|---| | type | yes | only "command" exists today | | command | yes | a script path, or a one-line inline shell command (e.g. jq) | | padding | no | horizontal padding in characters, defaults to 0 |

Three optional settings worth knowing: refreshInterval (re-run the script every N seconds, minimum 1 — for clock-style displays that events don't cover), hideVimModeIndicator (suppress the built-in -- INSERT -- text if your script renders vim.mode itself), and subagentStatusLine, a separate setting that customizes the subagent rows in the agent panel.

The fastest start is running /statusline inside Claude Code and describing what you want — it generates the config and script. Remove the status line with /statusline delete, or by deleting the statusLine field.

The stdin JSON protocol: what your script actually receives

On every refresh, Claude Code writes one JSON object to your script's stdin. These are the fields that matter (fields marked * may be absent entirely):

| Field | Type | Content | |---|---|---| | model.id / model.display_name | string | current model ID and display name | | workspace.current_dir | string | current working directory (more reliable than top-level cwd) | | workspace.project_dir | string | project root | | context_window.used_percentage | number | context window used, percent (may be null) | | context_window.remaining_percentage | number | remaining percent (may be null) | | cost.total_cost_usd | number | session cost in USD so far | | cost.total_duration_ms | number | wall-clock session duration in ms | | cost.total_lines_added / removed | number | cumulative lines added / removed | | output_style.name | string | current output style | | version | string | Claude Code version | | rate_limits * | object | Pro/Max 5-hour / 7-day windows (used_percentage, resets_at) | | prompt_cache * | object | cache telemetry: warm, hit_ratio, ttl, expires_at (v2.1.251+) | | vim.mode | string | NORMAL / INSERT / VISUAL | | pr * | object | associated PR: number, review state | | effort.level | string | thinking effort level (low → max) |

Two details that bite:

Fields can be absent — and null. rate_limits appears only for subscription users and only after the first API response; context_window.current_usage is null before the first API call and after every /compact. So use // 0 fallbacks in jq, or 0 in Python, and ?. in Node — unconditionally.

used_percentage counts input only. The formula is input_tokens + cache_creation_input_tokens + cache_read_input_tokens over context_window_size (200k by default, 1M extended) — output tokens are excluded. Match this when you compute headroom yourself, or your number will disagree with the official bar.

Three copy-paste script templates

Template 1: a jq one-liner — model name + context bar

No script file needed; command accepts an inline pipeline:

jq -r '"[\(.model.display_name)] \(.context_window.used_percentage // 0)%"'

The one most people actually want is the official context bar. Save it as ~/.claude/statusline.sh:

#!/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%"

Rendered: [Sonnet 4.5] ▓▓▓░░░░░░░ 32%. The // 0 guards the null early in a session; cut -d. -f1 truncates the decimal so shell arithmetic works.

Template 2: cost and duration

#!/bin/bash
input=$(cat)
COST=$(echo "$input" | jq -r '.cost.total_cost_usd // 0')
DUR=$(echo "$input" | jq -r '.cost.total_duration_ms // 0')
MIN=$((DUR / 60000)); SEC=$(((DUR % 60000) / 1000))
printf '💰 $%.2f | ⏱️ %dm %ds\n' "$COST" "$MIN" "$SEC"

total_cost_usd reflects what the API calls would have cost — for subscription users it's an equivalence figure rather than a bill. To show code churn as well, append .cost.total_lines_added and .cost.total_lines_removed.

Template 3: a multi-line status line (git + context + cost)

Multi-line output is officially supported: each echo becomes a row. Don't query git from scratch every refresh — cache git state to /tmp keyed by session_id with a ~5 second TTL, which is exactly what the official docs recommend:

#!/bin/bash
input=$(cat)
SESSION=$(echo "$input" | jq -r '.session_id')
DIR=$(echo "$input" | jq -r '.workspace.current_dir')
CACHE="/tmp/cc-git-${SESSION}"
MTIME=$(stat -f %m "$CACHE" 2>/dev/null || stat -c %Y "$CACHE" 2>/dev/null || echo 0)

if [ ! -f "$CACHE" ] || [ $(( $(date +%s) - MTIME )) -gt 5 ]; then
  (cd "$DIR" && git branch --show-current 2>/dev/null; git status --porcelain 2>/dev/null | wc -l) > "$CACHE"
fi
BRANCH=$(sed -n 1p "$CACHE"); DIRTY=$(sed -n 2p "$CACHE")

PCT=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
COST=$(echo "$input" | jq -r '.cost.total_cost_usd // 0')

echo "🌿 ${BRANCH:-no-git} · ${DIRTY} changed"
echo "[$(echo "$input" | jq -r '.model.display_name')] ctx ${PCT}% · \$$(printf '%.2f' $COST)"

Two rows: branch and dirty-file count on the first, model, context, and cost on the second. ANSI colors and OSC 8 clickable links are supported too, so you can add color on top of this skeleton.

Refresh timing and performance: when your script runs

The status line is not polled every second — it is event-driven: once at session start (including resume), then on a new assistant message, /compact completion, permission-mode change, vim-mode toggle, a change to the command setting, refreshInterval expiry, a rate-limit window hitting resets_at, or a warm cache hitting expires_at. Updates are debounced at 300ms, and if a new event fires while the previous script run is still going, the in-flight run is cancelled.

That leads to two performance rules:

  1. Slow scripts drop updates. Bare git status in a large repo is the classic offender — cache to /tmp as in template 3, keyed by session_id (not PID, which changes on every invocation).
  2. Clocks need refreshInterval. Without a new message the status line sits still; a clock, or state changes from background subagents while you idle, require the timed refresh.

Common pitfalls: why is my status line blank?

Pitfall 1: output went to stderr. The status line reads stdout only — debug prints on stderr, or a non-zero exit code, produce a blank row. Fix: confirm the final echo writes to stdout; run the script once in a plain shell to see what happens.

Pitfall 2: no null fallbacks. Early in a session many fields are null; jq -r '.context_window.used_percentage' returns the literal string null, arithmetic explodes, the script exits non-zero, and the row goes blank. Fix: // 0 on every numeric field, no exceptions.

Pitfall 3: tput cols doesn't work. Claude Code captures the script's output, so tput can't detect a terminal. Fix: read the COLUMNS / LINES environment variables instead.

Pitfall 4: workspace trust not accepted. In a fresh project the status line is blank with no error — the command is skipped until trust is accepted (claude --debug shows Status line command skipped: workspace trust not accepted). Fix: accept the workspace trust prompt.

Pitfall 5: Windows path backslashes eaten by Git Bash. An unquoted C:\Users\... in command loses its backslashes. Fix: use forward slashes (C:/Users/...) or ~.

Editing your script does not re-render instantly — changes take effect on the next trigger event.

Related guides

Frequently asked questions

Where does the statusLine config live, and what must type be?

In ~/.claude/settings.json (user-level) or the project-level settings.json. type only supports "command" today, and command takes either a script path or a one-line inline shell command. The fastest setup is running /statusline in Claude Code and describing what you want; /statusline delete removes it.

How often does the status line refresh?

It's event-driven: once at session start, then on new assistant messages, /compact completion, permission or vim mode changes, and similar events, debounced at 300ms. Nothing happens without a new event — for clock-style displays, configure refreshInterval (re-run every N seconds, minimum 1).

Why is my status line completely blank?

Check five things in order: the final output goes to stdout (stderr and non-zero exit codes blank the row), numeric fields have // 0 null fallbacks, workspace trust has been accepted (claude --debug shows the skip message), a disableAllHooks-style policy hasn't disabled it, and the script has no syntax error — running it once in a plain shell locates most of these fastest.

How is context_window.used_percentage computed?

The sum of the three input-side token counts (input_tokens + cache_creation_input_tokens + cache_read_input_tokens) divided by context_window_size (200k default, 1M extended). Output tokens are excluded. Match this formula when you estimate headroom yourself, or your number will disagree with the official bar.

How do I get the terminal width inside the script?

Read the COLUMNS / LINES environment variables. tput cols returns nothing useful here — Claude Code captures the script's output, so there is no terminal to query.

Are there ready-made status line tools?

Yes. The most popular is ccstatusline (12.7k stars as of September 2026), a configurable TUI customizer; claude-hud and the Rust-based CCometixLine are other options. If you want to understand every byte of your own status line, the three templates in this guide remain the most direct route.

Official references

This article reflects the public documentation as of September 1, 2026 (v2.1.251). Statusline fields are still evolving quickly — the official docs win.