statusLine: the Claude Code status line I rebuilt after a 13-second render
- claude-code
- agent-ops
- context-engineering
statusLine is the setting in Claude Code that runs a script of your choosing after
every assistant message and shows whatever it prints in a bar at the bottom of the
terminal. The docs describe the input as JSON session data on stdin: model, context
window, cost, rate limits, effort, open PR, worktree. What you print is your call.
For a while my call was a tool that took 13 to 20 seconds per render.
- What it is. One settings object,
statusLine, withtypeset tocommandand acommandto run. JSON in, stdout out. Per the docs it runs locally and spends no API tokens. - What the payload carries.
context_window.used_percentage,current_usagewith cache tokens split out,effort.level,rate_limits.five_hourandseven_daywithresets_at,pr,worktree,agent.name,cost. - What mine shows. Model, agent name, effort, a 10-cell context bar with percent and used/size, 5-hour and 7-day limits, a PR badge, worktree or directory. 171 lines of Node, zero I/O, median 41 ms.
- The detour.
ccusage statuslinescanned 916 files and 763 MB on every render, 13 to 20 seconds each. Its cache cut that to 0.27 s and, at a 60 s refresh interval, froze the context counter at50,000 (5%). - The rule. This is the one command whose output sits on the screen after every message. Time it on your own data before you trust it, and again when your history grows.
Same Claude Code install whose skills I sorted by blast radius in the disable-model-invocation post; of all its settings keys, this one is on screen all day.
How do I set up a status line in Claude Code?
Two ways, both from Claude Code's docs. Type /statusline with a plain-English
description and Claude Code writes a script into ~/.claude/ and updates your
settings. Or add a statusLine object to ~/.claude/settings.json yourself, with
type set to "command" and command pointing at a script or an inline shell
command. Settings reload on save.
The manual route:
- Write a script that reads JSON from stdin and prints a line. The docs' smallest
example uses
jqto pullmodel.display_name,workspace.current_dirandcontext_window.used_percentage. Test it with the docs' one-liner that pipes a hand-written JSON object into the script. - Make it executable with
chmod +xon macOS and Linux. On Windows, read the Windows section below first. - Add the object to
~/.claude/settings.json. Mine, path shortened:
{
"statusLine": {
"type": "command",
"command": "node C:/path/to/statusline.mjs"
}
}
Three optional fields sit next to command: padding for horizontal spacing,
default 0; refreshInterval to re-run every N seconds on top of the event-driven
updates, minimum 1, meant for clocks or for idle sessions where background
subagents change git state; hideVimModeIndicator for scripts that render
vim.mode themselves. /statusline delete removes the whole thing.
How does Claude Statusline work?
Claude Code runs your command with a JSON snapshot of the session on stdin and
displays whatever the script prints to stdout. It runs once at session start, then
again after every assistant message, after /compact, when the permission mode or
vim mode changes, and on a refreshInterval timer if you set one. Updates are
debounced at 300 ms.
The docs list eight triggers; the other three are a change to the command string
itself, which skips the debounce, a rate-limit window reaching its resets_at, and
a warm prompt cache reaching its expires_at. A new trigger cancels a script still
running. And from the troubleshooting section, the sentence that explains my whole
detour: slow scripts block the status line from updating until they complete.
The fields I care about, docs wording condensed:
| Field | What it holds, per the docs |
|---|---|
context_window.total_input_tokens | Tokens in the context window from the most recent API response, cache reads and writes included |
context_window.used_percentage | Pre-calculated; may be null early in the session |
context_window.current_usage | input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens; null before the first call and right after /compact |
effort.level | low, medium, high, xhigh or max; absent when the model has no effort parameter |
rate_limits.five_hour, rate_limits.seven_day | used_percentage from 0 to 100 and resets_at in epoch seconds; Claude.ai Pro and Max subscribers, or behind a Claude apps gateway, and only after the first API response |
Per the docs, total_input_tokens is the sum of input_tokens,
cache_creation_input_tokens and cache_read_input_tokens, and used_percentage
uses those three only, never output_tokens. This is the
context window counter most people put a
status line up for, measured in tokens the way the API
counts them, cache included.
What my status line shows
The script is 171 lines of Node. Its first comment is the design: zero I/O, read
the stdin payload and nothing else. No fs, no child_process, no git commands.
Five groups, left to right; a group with no data disappears.
- Identity.
model.display_name, thenagent.namein parentheses when the--agentflag or agent settings set one, theneffort.level. My settings pin effort atxhigh, so that word sits on the bar all day. - Context. A 10-cell bar, the percent, and
used/sizeinkorM. The percent comes fromused_percentage, withtotal_input_tokensovercontext_window_sizeas fallback. Green under 50 percent, yellow from 50, red from 80. Hidden whiletotal_input_tokensis zero. - Limits.
rate_limits.five_hourandseven_dayas percentages, green under 60, yellow from 60, red from 85. The countdown toresets_atshows only from 85 up, because a reset time is noise until the limit is a problem. - PR.
pr.numberasPR #n, orMR !nwhenpr.kindismr. Green onapproved, red onchanges_requested, dim otherwise. - Place.
worktree.namewith awt:prefix inside a worktree session; otherwise the last segment ofworkspace.current_dir.
If the payload fails to parse, the script prints an empty string: a blank bar beats
an error in the bar. Not shown: cost, the cache split from current_usage, and git
branch or dirty state. Branch and staged counts need git calls, which is I/O, and
I would rather have a bar that never touches the disk.
The ccusage detour: 13 seconds per render
Until 25 August 2026 the command in that settings key was ccusage statusline,
version 18.0.10. It shows the day's cost and the current 5-hour block. On my
machine a render took 13 to 20 seconds, and the control run says why: on every
render it reads every transcript under ~/.claude/projects, a directory that held
916 files and 763 MB.
Same payload, same command, pointed at a directory with a single transcript: 0.14 s. Pointed at the real directory: 13.2 s. The payload was never the problem; the history scan was.
ccusage has a cache, and the cache brought a render down to 0.27 s. With
--refresh-interval 60, I fed it three payloads carrying 50k, 300k and 777k
tokens, and all three came back as 50,000 (5%). That frozen counter says the
cache holds the whole output line, not the expensive part: the one number I put a
status line up for stood still for a minute at a time. At a 1 s interval the
counter stayed fresh, but every second or third render fell into the full scan
again.
So I replaced it with the script above: median render 41 ms, zero I/O. The price
was the dollar figures: the day total and the 5-hour block need the history scan by
definition, so they left the bar and live in /cost and a manual ccusage run.
The dead workaround: a grep through the binary
The second thing 25 August killed was older. On 16 April 2026 I had written
statusline-wrapper.mjs around a Claude Code bug: total_input_tokens did not
include cache tokens back then, so on a cached conversation the counter showed 0
percent. The wrapper read the transcript, added the cache tokens and passed the
corrected payload on.
Before deleting it I checked whether the bug still existed.
grep -a -o -b "total_input_tokens" claude.exe on the 337 MB
binary returned the implementation:
total_input_tokens: e.input_tokens + e.cache_creation_input_tokens + e.cache_read_input_tokens.
The docs say the same today. The workaround had been dead code for some time.
One more measurement, because I had been blaming the wrapper: reading a 59 MB transcript cost it 0.13 s, two orders of magnitude under the 13 s I was seeing. Measure before you delete the wrong thing.
statusline on Windows: the backslash problem
The docs' problem first: on Windows, Claude Code runs the status line command
through Git Bash when it is installed, and through PowerShell otherwise. Git Bash
treats unquoted backslashes as escape characters, so a command like
C:\Users\you\script.mjs reaches the runner with its separators stripped and fails
with no visible error. Write the path with forward slashes; ~ also works. A
PowerShell script runs through
powershell -NoProfile -File C:/Users/you/.claude/statusline.ps1.
Mine: an agent wrote the script through its Bash tool, which halved every run of
backslashes on the way to disk. Each \\ arrived as a single \. The regex that
splits a path on either separator, /[\\/]/, was saved as /[\/]/. node --check
passed, the script ran, and the status line showed the entire directory path
instead of its last segment. Three rules came out of it: forward slashes only in
agent-written scripts, String.fromCharCode(92) when a backslash is needed, and a
grep of the critical line after saving, because a syntax checker cannot catch a
regex that is valid and wrong. The script now contains no literal backslash at all.
Why is my statusline not showing?
The docs' troubleshooting list, in the order I would check it. The script must be
executable and print to stdout, not stderr. A non-zero exit or empty output blanks
the line. On Windows with Git Bash, backslashes in the command path are the usual
cause.
The status line runs under the same workspace trust rule as hooks, so until you
accept the trust dialog it stays blank, and claude --debug logs
Status line command skipped: workspace trust not accepted. Two settings can also
kill it: disableAllHooks outside managed settings, and allowManagedHooksOnly
set by an organization.
If the line shows but the numbers look wrong: fields may be null before the first
API response, current_usage goes null again right after /compact, and the
context percentage can differ from /context because the two are calculated at
different moments. /context shows the same metric on demand, and /doctor is the
command I lined up for the CLAUDE.md audit; the status
line is the live version of the first.
What is the best statusline in Claude Code?
The best status line is the one that reads only stdin. Everything you need for context, effort, rate limits, PR state and worktree is already in the payload, and a script that touches nothing else renders in milliseconds. Any tool that scans your history on each render gets slower as you use Claude Code more. Mine did.
One keyword tool estimates 1,100 searches a month for "claude code statusline" as a twelve-month average, 738 last month, keyword difficulty 8. Page one of Google on 30 August 2026: the docs page, ccstatusline, statusline.sh (a drag-and-drop builder), ccusage's beta status line integration and a few personal write-ups.
I have not run ccstatusline or statusline.sh, so no verdict on either beyond the
test I would apply to any of them: pipe the docs' mock payload in, time it, then
point it at your real history and time it again. Over a second on the second run is
the wrong tool for this slot. If it needs a cache to be fast, check what the cache
freezes. The zero-effort answer is /statusline itself: a script that reads stdin
and nothing else.
Do Cursor, Codex, Copilot and opencode have a status line?
Three of the four do. Codex shows built-in items you pick
and reorder with /statusline. Cursor's CLI has one on/off switch, for elapsed time in the bar.
GitHub Copilot CLI has a configurable footer and a statusLine.command hook that
takes session JSON on stdin. opencode has no status line key in its docs.
Vendor pages re-read 15 September 2026; the GitHub Copilot CLI references were re-read on 18 September 2026.
| Agent | Status line | What the docs say |
|---|---|---|
| Claude Code | Yes, your script | Your command gets session JSON on stdin; stdout is the bar. |
| Codex CLI | Yes, built-in items | tui.status_line, array<string> or null: "Ordered list of TUI footer status-line item identifiers. null disables the status line." |
| Cursor CLI | Yes, one switch | display.showStatusLineRunningTime: "Show elapsed running time in the status line". No script hook. |
| opencode | No key in the docs | tui.json covers scrolling, diff style, cursor, mouse and attention alerts. No status line entry. |
| GitHub Copilot CLI | Yes, built-in items or your script | /statusline and /footer configure the bar; statusLine.command receives session JSON on stdin and prints status content to stdout. |
| VS Code Copilot | Not checked | Not read for this post. |
opencode statusline: what exists and what does not
The opencode config page and
TUI page both say "Last updated: Sep 14,
2026"; neither contains status line, status bar or footer. TUI settings live in
their own tui.json: scroll_speed, scroll_acceleration, diff_style,
cursor.style and cursor.blinking, mouse, and an attention block for
notifications and sound. The TUI page is seventeen slash commands and keybinds. So
as of 14 September 2026 an opencode statusline search gets a theme and a keymap
from the docs, not a configurable bar.
Codex status line: tui.status_line
/statusline landed in openai/codex on
5 February 2026: a picker whose order is saved to tui.status_line in
~/.codex/config.toml. The
developer commands page lists
the choices: model, model with reasoning, context stats, rate limits, git branch,
token counters, session id, current directory or project root, Codex version. The
config reference
gives the type, no identifiers, no default; ["spinner", "project"] on that page
is the tui.terminal_title default, a different key. Mine, checked 15 September
2026, holds six: model-with-reasoning, context-used, used-tokens,
five-hour-limit, weekly-limit, current-dir. What Codex lacks is a command
hook: issue #17827 asks for one
and has been open since 14 April 2026.
And ccstatusline for Codex: its
README calls it "A highly
customizable status line formatter for Claude Code CLI", it installs into Claude
Code's statusLine setting.
The verdict
statusLine is a small setting with an unusual property: whatever you put there
runs after every assistant message, and its output stays on screen all session.
That makes it one place where a tool's cost scales with how much you use the
agent, and my numbers say the scaling is real: 916 files, 763 MB, 13 to 20 seconds
per render, a cache that froze the number I was watching, a workaround dead since
the payload started summing cache tokens. The replacement is 171 lines that read
stdin and print a line in 41 ms. Measure the thing in the bar on your own history,
then again in three months.
Sources read on 30 August 2026; the cross-agent section was re-read on 15 September 2026. GitHub Copilot CLI references re-read on 18 September 2026. Customize your status line in Claude Code's official docs, source of every field name and trigger above; the Cursor CLI configuration reference; the Codex config reference and CLI customization pages; the opencode config page; and the GitHub Copilot CLI settings page. The numbers about my own setup come from my session notes of 25 August 2026 and from the script itself.