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:
| Field | What it does |
|---|---|
padding | Extra horizontal spacing in characters, on top of the interface's own. Defaults to 0 |
refreshInterval | Re-runs the command every N seconds on top of event-driven updates. Minimum 1. Set it when you display a clock or a countdown |
hideVimModeIndicator | Suppresses 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:
| Field | What it carries |
|---|---|
cwd, workspace.current_dir | The current directory |
workspace.project_dir | The directory the session started in |
workspace.repo | host, owner and name, inside a git repo with an origin remote |
session_id, session_name | The session identifier, and its custom or AI-generated name |
model.id, model.display_name | For example claude-opus-5 and Opus |
version | The Claude Code version |
output_style.name | The active output style |
context_window.used_percentage | Context used, 0 to 100. Also remaining_percentage, context_window_size and a current_usage breakdown |
exceeds_200k_tokens | Whether the conversation went past 200k |
cost.total_cost_usd | Session cost, plus total_duration_ms, total_lines_added and total_lines_removed |
effort.level | Reasoning effort, such as high or max |
thinking.enabled, fast_mode | Booleans |
rate_limits.five_hour | used_percentage and resets_at, a Unix timestamp in seconds. Same shape for seven_day |
prompt_cache.hit_ratio | Prompt cache statistics for the main conversation |
vim.mode | NORMAL, INSERT, VISUAL or VISUAL LINE |
agent.name, pr.number, worktree.name | The 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_nameappears only when a custom name is set with--nameor/rename, or once an AI-generated title exists. The default display name likemy-app-3fdoes not populate it.effortappears only when the model supports a reasoning effort parameter.workspace.reponeeds a git repo with anoriginremote, andworkspace.git_worktreeneeds a linked worktree.vim,agent,prandworktreeappear only in their respective modes.rate_limits.spend_limitandprompt_cacherequire Claude Code 2.1.251 or later,prompt_id2.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:
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