One line at the bottom of the Claude Code TUI: context-window usage, cwd, model state, and — only when it is about to bite — a Claude.ai plan limit.
[██████····] 118k/200k 59% · dotfiles/.config/claude · Opus 5 1M/high fast
- context window — a 10-cell bar plus
used/sizeand a percentage, green under 60%, yellow from 60%, red from 80%. - cwd — relative to the project root when you're inside it (shown as
<project-leaf>/rest), otherwise~-relative, otherwise absolute. - model — display name with
(1M context)collapsed to1M, then the reasoning effort, and thefast/no-thinktoggles when they're on. - plan limits — the 5-hour and 7-day subscription windows. Usually absent: a window shows up only when the current pace would exhaust it before it resets. See below. Never shown at all for API-key users, or before the session's first API response.
Two implementations of the same output: statusline.sh (POSIX sh + jq) for
macOS and Linux/WSL, statusline.ps1 (PowerShell) for Windows. Pick the one
matching your box — they don't depend on each other.
This segment is an alarm, not a gauge. A percentage on its own was never worth the space — 90% with ten minutes left on the window is fine, 90% with four hours left is not — and what actually costs you is neither number but the gap between them. So nothing is shown while the pace is survivable, and when it is not:
[██████····] 118k/200k 59% · dotfiles · Opus 5 1M/high · 5h 71% 🚧 1h25m ⏳ 4m
- 🚧 — the roadblock you are heading for: how long the budget lasts at the current pace, before the cap stops you.
- ⏳ — how long you would then sit waiting, from hitting the cap to the reset. It trails because the pair reads in the order it happens, but it is the number that hurts and the one the colour encodes: green under 20 minutes, yellow under 90, red past that. Absolute rather than a fraction of the window — an hour of waiting is an hour whichever window ran out, which is why the 7-day window is almost always red once it trips at all.
Both glyphs come from the emoji font rather than the terminal font, so they are two cells wide and the space after each is load-bearing — without it the glyph overlaps the following digit. Verified by screenshot under PragmataPro + Noto in konsole; substitute plain words if your terminal disagrees.
The pace is the average since the window opened. Two things follow:
- The window's start is derived, not given. The payload carries only
resets_at, so the start isresets_atminus a fixed length — 5h forfive_hour(hour-aligned; verified against a real payload, a reset at 05:00 with 3h02m left puts the start at 00:00), 7 days forseven_day. If the time to reset ever exceeds that length the assumption has broken, and nothing is shown rather than something guessed at. - It says nothing early on. In the first 5% of a window two minutes of typing extrapolates to a catastrophe, so no window is evaluated until then.
Being an average, it lags: a burst at the start of a window keeps weighing on the estimate long after you have stopped, and the alarm can hang around after you have already slowed down. Reading a recent rate instead would mean keeping samples between renders, which neither script does.
Save the script somewhere stable and make it executable:
chmod +x /path/to/statusline.shThen register it in ~/.claude/settings.json:
{
"statusLine": {
"type": "command",
"command": "/absolute/path/to/statusline.sh"
}
}The path must be absolute. The command string is handed to a shell whose
identity isn't guaranteed, so ~ and $HOME may not expand. On Windows the
command is pwsh -NoProfile -File C:\absolute\path\to\statusline.ps1.
If you run more than one Claude Code profile (a second CLAUDE_CONFIG_DIR,
e.g. a separate work profile), each profile's settings.json needs its own
registration.
Claude Code pipes a JSON status payload to the script on stdin. The fields these scripts read:
| field | used for |
|---|---|
context_window.context_window_size |
bar denominator; everything context-related is skipped when absent or 0 |
context_window.total_input_tokens |
bar numerator |
context_window.used_percentage |
percentage — null until the first API response of the session, so both scripts fall back to computing it from the two counts |
cwd |
current directory |
workspace.project_dir |
project root, for the relative path |
model.display_name |
model name |
effort.level |
reasoning effort |
fast_mode |
the fast toggle |
thinking.enabled |
rendered as no-think when explicitly false |
rate_limits.five_hour.{used_percentage,resets_at} |
the 5h window |
rate_limits.seven_day.{used_percentage,resets_at} |
the 7d window |
This is not a documented contract. These names were read out of the CLI
binary (2.1.218; rate_limits re-checked against 2.1.231) rather than from any
published schema, so they can change without notice. Both scripts are written to
degrade rather than break: a field
that's missing drops its segment, and unparseable input prints nothing and
exits 0. Still worth re-checking against a real payload after a CLI upgrade —
dump one with a temporary statusLine command of cat > /tmp/payload.json.
statusline.sh does all rendering inside jq, so there's no shell arithmetic
or word-splitting to get wrong; the shell part is a single jq -r invocation.
It needs jq on PATH.
statusline.ps1 forces [Console]::OutputEncoding to UTF-8 without a BOM.
When stdout is a pipe rather than a console, PowerShell defaults to the OEM code
page (CP437 on the box this was written on), which encodes the bar's █ as a
bare 0xDB — not valid UTF-8, so the whole line renders as tofu.
Path handling accepts either separator on both scripts, so a POSIX-shaped payload works under a Windows-installed pwsh and vice versa.
Public domain / CC0. Take it, change it, no attribution needed.