What the Claude Code status line is
The status line is the single line Claude Code pins to the bottom of your terminal — the same idea as a shell prompt or a tmux status bar, but scoped to your session with Claude. Out of the box it's minimal. With one setting, it becomes yours: which model is on duty, what branch you're about to let an AI loose on, what this session has cost so far — whatever you can print, it can show.
The mechanism is refreshingly blunt: you point Claude Code at any executable. It runs your command, feeds it a JSON blob of session state on stdin, and displays whatever your command prints. That's the whole contract. Everything on this page is detail.
Three ways to set one up
1. Ask Claude Code to do it (easiest)
Claude Code has a built-in /statusline command that takes plain-English
instructions and writes the script for you. That's what the generator above produces: a
precise instruction, so you get exactly the line you designed instead of whatever the
model improvises. Paste, press enter, done.
2. Use a ready-made script
Flip the output above to Script + settings.json for a finished shell script and the setting that wires it in. You see every line before it runs — the reasonable instinct for anything that executes on every update.
3. Write your own
Any executable works — bash, Python, Node, a compiled binary. Read JSON from stdin, print one line to stdout. The stdin reference below is everything your script will be handed.
statusLine in settings.json
The setting lives in ~/.claude/settings.json (all sessions) or a
project's .claude/settings.json (that project only — handy for a
per-repo status line):
{
"statusLine": {
"type": "command",
"command": "~/.claude/statusline.sh",
"padding": 0
}
}
| Key | Type | What it does |
|---|---|---|
type | string | Always "command" — the status line is produced by running a command. |
command | string | The executable to run. ~ expands; relative paths are asking for trouble — use absolute or ~-anchored. |
padding | number | Optional, default 0. Adds extra horizontal spacing (in characters) on top of the interface's built-in spacing — it controls relative indentation, not distance from the terminal edge. |
refreshInterval | number | Optional. Re-runs the command every N seconds (minimum 1) in addition to event-driven updates. Set it when your line shows a clock or other time-based value; leave it unset to run only on events. |
What your command receives on stdin
On every update, Claude Code pipes a JSON object like this to your command:
{
"session_id": "abc123-...",
"transcript_path": "/Users/you/.claude/projects/.../transcript.jsonl",
"cwd": "/Users/you/projects/my-app",
"version": "2.1.205",
"model": {
"id": "claude-opus-4-8",
"display_name": "Opus"
},
"workspace": {
"current_dir": "/Users/you/projects/my-app",
"project_dir": "/Users/you/projects/my-app"
},
"output_style": { "name": "default" },
"cost": {
"total_cost_usd": 0.42,
"total_duration_ms": 754321,
"total_api_duration_ms": 98765,
"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
},
"exceeds_200k_tokens": false
}
| Field | What it is |
|---|---|
model.display_name | Human-friendly model name ("Opus", "Sonnet"). The one you want on screen. |
model.id | Full model identifier — for scripts that branch on exact model. |
workspace.current_dir | Directory the session is currently working in. Basename it for a compact display. |
workspace.project_dir | The project root the session started in. Differs from current_dir when Claude wanders. |
cwd | Same as workspace.current_dir; kept for compatibility. |
version | The Claude Code version string. |
output_style.name | Active output style (e.g. default). |
cost.total_cost_usd | Session spend so far, in USD. The number worth watching. |
cost.total_duration_ms | Wall-clock session length, milliseconds. |
cost.total_api_duration_ms | Time spent waiting on the API specifically. |
cost.total_lines_added / total_lines_removed | Lines of code Claude has added / removed this session. |
context_window.used_percentage | Pre-calculated percentage of the context window in use. The ready-made number for a context gauge — no transcript parsing required. May be null before the first API response. |
context_window.remaining_percentage | The complement — percentage of context still free. |
context_window.context_window_size | Maximum context window in tokens (200000, or 1000000 for extended-context models). |
context_window.total_input_tokens / total_output_tokens | Tokens currently in the context window, from the most recent API response. |
exceeds_200k_tokens | Boolean — true when the most recent response's combined tokens exceed 200k (a fixed threshold, regardless of window size). |
session_id, transcript_path | Identifiers for advanced scripts — e.g. parsing the transcript, or keying a per-session cache off session_id. |
How rendering actually works
- Every line you print is a row. A single line is the common case
(and all this generator makes), but each additional
echorenders as its own row above the built-in footer — handy for a second line with a context bar. Keep it tight; the status area has limited width. - Updates are event-driven, debounced at ~300 ms. The line
refreshes after each new assistant message, after
/compact, and on permission- or vim-mode changes — not on a clock. So a "live" seconds counter won't tick between messages unless you setrefreshInterval(see the settings table) to also re-run on a timer. - ANSI escape codes render.
\033[36mgets you cyan, not garbage. Dim (\033[2m) is the workhorse — status lines read best quiet. - It runs in your session's directory, so
git branch --show-currentand friends work with no extra plumbing. - Silence is allowed. Empty output or a failing command just means no status line — Claude Code carries on.
Worked examples
The minimalist
Model and folder. Enough to never again wonder which terminal tab is burning Opus tokens.
#!/usr/bin/env bash
input=$(cat)
model=$(echo "$input" | jq -r '.model.display_name')
dir=$(basename "$(echo "$input" | jq -r '.workspace.current_dir')")
printf '%s │ %s' "$model" "$dir"
The git-aware
Adds the branch, with a * when the tree is dirty — the single most useful
character an AI-assisted coder can keep in view.
#!/usr/bin/env bash
input=$(cat)
model=$(echo "$input" | jq -r '.model.display_name')
cwd=$(echo "$input" | jq -r '.workspace.current_dir')
branch=$(git -C "$cwd" branch --show-current 2>/dev/null)
dirty=$(git -C "$cwd" status --porcelain 2>/dev/null | head -1)
[ -n "$dirty" ] && branch="${branch}*"
out="$model"
[ -n "$branch" ] && out="$out │ $branch"
printf '%s' "$out"
The spend-watcher
Cost, session length, and the diff Claude has racked up. For anyone on a budget — which is everyone.
#!/usr/bin/env bash
input=$(cat)
cost=$(echo "$input" | jq -r '.cost.total_cost_usd // 0' | awk '{printf "$%.2f", $1}')
mins=$(( $(echo "$input" | jq -r '.cost.total_duration_ms // 0') / 60000 ))
added=$(echo "$input" | jq -r '.cost.total_lines_added // 0')
removed=$(echo "$input" | jq -r '.cost.total_lines_removed // 0')
printf '%s │ %sm │ +%s -%s' "$cost" "$mins" "$added" "$removed"
Troubleshooting
| Symptom | Fix |
|---|---|
| Nothing shows up at all | Usual suspects: the script isn't executable (chmod +x), the command path is wrong (test with echo '{}' | ~/.claude/statusline.sh), the script prints to stderr instead of stdout, or the workspace-trust prompt hasn't been accepted (the status line needs the same trust as hooks). Run claude --debug to see the exit code and stderr. |
jq: command not found | Install it (brew install jq / apt install jq) — or ask Claude Code to rewrite the script without the dependency; it's happy to. |
Literal \033[36m in the line | Your echo isn't interpreting escapes. Use printf, or bash's $'\033[36m' quoting, not plain echo. |
| Values feel stale | By design: updates ride conversation events with a ~300 ms debounce, so clocks and tickers sit still between messages. Add "refreshInterval": 1 to statusLine to also refresh on a timer. |
| Works globally, wrong per-project | A project's .claude/settings.json overrides ~/.claude/settings.json. Check which file actually holds statusLine. |
FAQ
Will a status line slow Claude Code down?
Not meaningfully — it runs outside the conversation loop. Keep the script itself snappy (under ~100 ms, no network calls) and you'll never notice it. A slow script doesn't block Claude; it just lags its own display.
Does it have to be bash?
No. Anything executable works: Python, Node, Rust, a one-line jq
invocation. Read stdin, print one line. Bash is just the lingua franca of examples.
Can different projects have different status lines?
Yes — put statusLine in the project's .claude/settings.json
and it wins over your global one in that repo. A loud red custom tag in the
production-infra repo is a genuinely good idea.
Can it show context-window usage?
Yes, and it's easy — Claude Code hands you a pre-calculated
context_window.used_percentage (and remaining_percentage), so
no transcript parsing is needed:
pct=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
printf '%s%%' "$pct"
Tick Context used in the generator above to fold it into your line, or
ask /statusline for "model name and context percentage with a progress bar".
The value may be null until the first API response — the // 0
fallback covers that.
Does this work on Windows?
Under WSL, exactly as shown. In native Windows shells, point
command at anything your shell can run — a PowerShell script reading stdin
works the same way.
Is anything I do on this page sent anywhere?
No. The generator is a static page; your selections never leave the browser. The script it produces runs only on your machine.