The Status Line

Customising the always-visible status line — model, branch, token usage, custom indicators. Useful for long sessions.

Feature Intermediate
8 min read
status-line ui customization long-sessions

What it is#

The status line is the always-visible strip of metadata Claude Code renders below (or alongside) the conversation. It shows the things the user wants to glance at without typing a command:

  • The active model (Opus, Sonnet, Haiku — or a specific point release).
  • The current working directory and git branch.
  • Token usage and the size of the context window the session has consumed.
  • Permission mode (default, accept-edits, plan, bypass).
  • Any custom indicators the user has wired in.

The status line is the lightweight observability surface for a session. The conversation tells you what the model is doing; the status line tells you what state the session itself is in. Both matter, but the status line matters most in long sessions, where “how much context am I burning?” and “what model am I actually talking to?” become recurring questions.

When to use it#

Customise the status line when:

  • You run long sessions and the default fields are not enough. Adding token-usage percentage and an indicator for “compaction has happened” gives you a feel for the session’s health.
  • You switch models mid-session and want to be sure which one is active before composing the next prompt.
  • You work across multiple repos and want the branch and dirty/clean state visible without git status.
  • You have a project-specific signal you need at a glance — a deploy lock, a feature flag state, a stale env file.

Stick with the default when:

  • You run short sessions. The default fields are tuned for the common case.
  • You haven’t felt the pain. Customisation is best driven by an actual recurring friction, not pre-emptive design.

How it works#

The default fields#

Out of the box, the status line shows (configurable but typical):

FieldMeaning
ModelThe active model name.
ModePermission mode (default / accept-edits / plan / bypass).
BranchCurrent git branch and dirty/clean indicator.
TokensTokens used in this session and a rough percentage of context window.
Cost(Optional) running USD cost.

The exact set varies by CLI version. The /help command and /config surface what is currently displayed.

Custom status line scripts#

A custom status line is a shell script that the harness runs on a refresh cadence (typically every few seconds, or after each turn — see your CLI version for specifics). The script receives session metadata on stdin (JSON: model, branch, token counts, mode, etc.) and emits a single line of text to stdout. That string becomes the status line content.

Schema (illustrative):

{
"model": "claude-opus-4-7",
"mode": "default",
"cwd": "/Users/me/repos/my-app",
"branch": "feature/new-auth",
"dirty": true,
"tokens": { "used": 41200, "window": 200000 },
"cost_usd": 0.18
}

The script reads stdin, picks the fields it wants, formats them, and prints them. The harness puts that string in the status row of the UI.

Refresh cadence#

The status line is not updated on every keystroke — that would thrash. It is refreshed on a cadence: between turns, after tool calls, and on a slow background timer for fields that don’t change often (token usage, cost). Custom scripts inherit this cadence. A 5-second status script does not stall the conversation — the harness times it out and uses the previous value.

Where it sits in the UI#

The status line is one row, fixed-width-friendly, ANSI-colour capable. The CLI renders it consistently across the terminal width; very long custom strings get truncated with an ellipsis. Plan for that: put the most important field first.

Configuration#

Toggling default fields#

settings.json controls which default fields appear:

{
"statusLine": {
"model": true,
"mode": true,
"branch": true,
"tokens": true,
"cost": false
}
}

A user who finds cost distracting can disable it; one who lives in long sessions can enable it. Defaults are sensible; this is just for the rare case where a field is noise.

Pointing at a custom script#

To replace the default rendering with a custom script:

{
"statusLine": {
"command": "~/.claude/status-line.sh"
}
}

When command is set, the harness uses the script instead of the built-in renderer. The default fields’ true/false switches no longer apply — the script controls the full string.

Script location and permissions#

Custom status scripts live anywhere the user has read+execute permission. By convention, ~/.claude/status-line.sh or ~/.claude/bin/status-line.sh. Mark it executable (chmod +x); the harness runs it directly.

Examples#

A minimal custom script#

Shows model, branch (with a * if dirty), and a percentage of the context window used.

~/.claude/status-line.sh
#!/usr/bin/env bash
payload="$(cat)"
model=$(jq -r '.model' <<<"$payload")
branch=$(jq -r '.branch' <<<"$payload")
dirty=$(jq -r '.dirty' <<<"$payload")
used=$(jq -r '.tokens.used' <<<"$payload")
window=$(jq -r '.tokens.window' <<<"$payload")
pct=$(( used * 100 / window ))
marker=""
[ "$dirty" = "true" ] && marker="*"
printf "%s | %s%s | ctx %d%%" "$model" "$branch" "$marker" "$pct"

Output: claude-opus-4-7 | feature/new-auth* | ctx 21%.

Add a deploy-lock indicator#

Some projects have a “do not deploy now” sentinel file (a lock the team uses during incident response). Surface it in the status line so the model and the user both know.

#!/usr/bin/env bash
payload="$(cat)"
model=$(jq -r '.model' <<<"$payload")
branch=$(jq -r '.branch' <<<"$payload")
lock=""
if [ -f /tmp/deploy-lock ]; then
lock=" [DEPLOY LOCKED]"
fi
printf "%s | %s%s" "$model" "$branch" "$lock"

When the lock file exists, the status line ends with [DEPLOY LOCKED]. The user spots it without running ls; the model sees it on the next turn if the status is fed into prompts (per the CLI’s behaviour).

Token-budget warning at 70%#

Long sessions degrade as context fills. A simple visual cue: change the colour or append a warning at thresholds.

#!/usr/bin/env bash
payload="$(cat)"
used=$(jq -r '.tokens.used' <<<"$payload")
window=$(jq -r '.tokens.window' <<<"$payload")
pct=$(( used * 100 / window ))
warn=""
if [ "$pct" -ge 90 ]; then
warn=" CONTEXT-90"
elif [ "$pct" -ge 70 ]; then
warn=" ctx-70"
fi
printf "ctx %d%%%s" "$pct" "$warn"

At 70% the line softly warns; at 90% it shouts. Either tells you to compact or hand off before the session degrades.

A multi-piece status line#

Combine fields with separators that read well on a 120-column terminal.

#!/usr/bin/env bash
payload="$(cat)"
model=$(jq -r '.model' <<<"$payload" | sed 's/claude-//')
branch=$(jq -r '.branch' <<<"$payload")
mode=$(jq -r '.mode' <<<"$payload")
pct=$(( $(jq -r '.tokens.used' <<<"$payload") * 100 / $(jq -r '.tokens.window' <<<"$payload") ))
# Compact form for narrow terminals
printf "%s | %s | %s | %d%%" "$model" "$mode" "$branch" "$pct"

Output: opus-4-7 | default | feature/auth | 18%.

Gotchas#

  • Slow scripts get timed out. The harness will not let the status script stall the UI. If your script takes more than a fraction of a second, the previous value is shown. Avoid network calls and slow git commands; cache where possible.
  • Stdout is the full string. Anything you print becomes the status line. Debug echo calls leak into the UI. Use stderr for any logging (which the harness routes to a debug log, not the status row).
  • JSON schema can change between CLI versions. Check the docs for your version before assuming a field exists. Defensive jq (// "unknown") saves you when a field is missing.
  • Don’t include secrets. The status line is visible in screenshots, shared screens, and shoulder-surfing. No tokens, no credentials, no internal hostnames you don’t want logged.
  • Terminal width matters. A 120-column terminal renders more than a 60-column one. Most-important-first ordering means the truncation cuts the least useful fields.
  • ANSI colour is supported but tricky. Use sparingly. A line that hard-blinks in red on every refresh is exhausting; a colour cue at thresholds works better.
  • The status line is not where to put long messages. It is one row. For longer signals, write them into the conversation or surface them via a hook.
  • Refresh cadence is not real-time. Don’t treat the status as a stopwatch. Token counts update between turns; mid-turn deltas may not surface until the next refresh.

Default vs custom status line at a glance#

Default status line
  • Tuned for the common case
  • No setup
  • Fields toggle on/off via settings
  • Right for short sessions and casual use
  • Updates on a sensible cadence automatically
Custom status line
  • Full control: any field, any ordering, any colour
  • Requires a script and the JSON schema
  • Right for long sessions and project-specific signals
  • You’re responsible for keeping it fast
  • One script can serve every project once it reads the cwd
Why long sessions reward an investment in the status line

In a 30-minute session, you can hold the model, the branch, and the rough token budget in your head — the status line is a convenience. In a 3-hour session with three sub-agents, two background Bash jobs, and a compaction halfway through, you can’t hold any of it. You start asking the model “what model are you?” and “how much context have we used?”, which both eat turns and surface stale answers. A custom status line that puts model, mode, dirty-branch, and context-percent in one row turns those questions into a glance. The longer your sessions, the bigger the payoff. Engineers who default to half-day sessions invariably end up with a tuned status line; engineers who don’t never feel the need.

Search ESC

Keyboard shortcuts

Shortcuts are disabled while typing in inputs.