← All guidesConfig

Claude Code statusline: the data your script gets

A Claude Code statusline is a shell script. Claude Code runs it, hands it a JSON payload on standard input, and renders whatever the script prints. That is the entire contract, and it is worth knowing because the payload carries far more than the model name, and because one of its fields lies to you when a session sits idle. If you have not set one up yet, it lives in your settings.json alongside your hooks.

Configure it

Add a statusLine block to ~/.claude/settings.json. Set type to command and point command at a script:

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

The command runs in a shell, so an inline one-liner works too. Three optional fields are easy to miss:

FieldWhat it does
paddingExtra horizontal spacing in characters, on top of the interface's own. Defaults to 0
refreshIntervalRe-runs the command every N seconds on top of event-driven updates. Minimum 1. Set it when you display a clock or a countdown
hideVimModeIndicatorSuppresses the built-in -- INSERT -- line when your script renders vim.mode itself

Claude Code reloads settings on save, so the statusline appears as soon as you write the file.

What your script receives

The payload is one JSON object on stdin. These are the fields worth reading:

FieldWhat it carries
cwd, workspace.current_dirThe current directory
workspace.project_dirThe directory the session started in
workspace.repohost, owner and name, inside a git repo with an origin remote
session_id, session_nameThe session identifier, and its custom or AI-generated name
model.id, model.display_nameFor example claude-opus-5 and Opus
versionThe Claude Code version
output_style.nameThe active output style
context_window.used_percentageContext used, 0 to 100. Also remaining_percentage, context_window_size and a current_usage breakdown
exceeds_200k_tokensWhether the conversation went past 200k
cost.total_cost_usdSession cost, plus total_duration_ms, total_lines_added and total_lines_removed
effort.levelReasoning effort, such as high or max
thinking.enabled, fast_modeBooleans
rate_limits.five_hourused_percentage and resets_at, a Unix timestamp in seconds. Same shape for seven_day
prompt_cache.hit_ratioPrompt cache statistics for the main conversation
vim.modeNORMAL, INSERT, VISUAL or VISUAL LINE
agent.name, pr.number, worktree.nameThe active agent, the open pull request, the worktree

The fields that are not always there

This is where most statusline bugs come from. Several keys are absent, not null, and a script that assumes them prints nothing at all:

  • session_name appears only when a custom name is set with --name or /rename, or once an AI-generated title exists. The default display name like my-app-3f does not populate it.
  • effort appears only when the model supports a reasoning effort parameter.
  • workspace.repo needs a git repo with an origin remote, and workspace.git_worktree needs a linked worktree.
  • vim, agent, pr and worktree appear only in their respective modes.
  • rate_limits.spend_limit and prompt_cache require Claude Code 2.1.251 or later, prompt_id 2.1.196 or later.

Guard every read. In jq, that means // "" or // 0 on each field.

A minimal statusline

This prints context, the five hour quota, the model and the folder:

#!/bin/bash
input=$(cat)
jq -r '
  "Ctx: \(.context_window.used_percentage // 0 | floor)%"
  + " | 5h: \(.rate_limits.five_hour.used_percentage // 0 | floor)%"
  + " | \(.model.display_name // "?")"
  + " | \(.workspace.current_dir | split("/") | last)"
' <<< "$input"

Fed a full payload, it prints:

Ctx: 42% | 5h: 23% | Opus 5 | my-app

There is a lesson in the degraded case. Run it against a payload with no rate_limits at all and you get:

Ctx: 8% | 5h: 0% | Opus 5 | my-app

A quota of 0% is a comfortable, wrong answer: the field was missing, not empty. Prefer hiding a block you cannot measure over printing a default that reads as good news.

Rate limits go stale, and that is the interesting part

rate_limits is only refreshed on an API exchange. A session you leave open without talking to it keeps serving the same snapshot for as long as it sits there, and the numbers drift further from reality with every hour.

Measured across ten sessions open at once on a single account, the spread reached 5.8 days of drift, with one session reporting the seven day window at 9 percent while the true figure was 27 percent. The quota is account-wide, so every one of those sessions was describing the same reality and only one of them was right.

There is a second, quieter detail. When five_hour.resets_at is already in the past, no window is running at all: a five hour window starts on a message, not on a fixed grid. Verified over a run of real payloads, resets_at lands on minutes like :00, :20, :30 and :50, which tracks the time of the first message rather than any clock boundary. So an elapsed resets_at does not mean "stale and useless", it means nothing has been spent since, and the remembered figure still holds.

The fix is a shared cache. Every session writes its snapshot to one file, and whichever session holds the freshest one publishes it for the others to read. Freshness ranks on five_hour.resets_at * 1000 + five_hour.used_percentage: resets_at increases from one window to the next, and within a window the percentage only rises, so the key is monotonic and the tie-break is free.

The statusline this site runs

The full script is here, MIT, no dependency beyond bash and jq:

Download statusline.sh

Save it to ~/.claude/statusline.sh, chmod +x it, and point your settings at it. It prints context, both quota windows with their countdowns, the model with its effort level, and the session name. On top of the plain readout it adds:

  • Two orthogonal quota signals. Colour is a forecast: it compares quota remaining against time remaining in the window, so it warns when the current burn rate will hit the wall early, not merely when a number is large. A reverse video block is a statement of present fact at 90 percent used or more, which a healthy forecast must never mask.
  • Context tiers at 60 percent (yellow), 80 percent (red) and 90 percent (reverse block).
  • A tilde in front of a figure that was deduced from an elapsed window rather than measured.
  • The cross-session cache described above.

Verified on 2026-09-13 against bash 5.2.21 and jq 1.7: five runs complete in 62 ms total, roughly 12 ms each, which matters because this script runs on every event.

One line per session is still one session

A statusline is the right tool for the session you are looking at. It is the wrong tool for the other four, because each one is drawn at the bottom of its own terminal, and the one that needs you is the one you are not looking at. That is the gap: the more parallel sessions you run, the less a per-terminal readout helps.

Blooby takes the same session state out of the terminal and puts it on your desktop, one animated mascot per session, so the one waiting on a permission raises its head whatever window you are in.

See it for yourself

download Get Blooby free