Unwait

Claude Code status line: what actually earns a row

· 6 min read claude code statusline configuration workflow

The statusline script receives about forty fields and most setups display the four least useful ones. What to show instead (rate limit burn, context percentage, session identity), the performance trap that makes it go stale, and the gotchas that leave it blank.

The Claude Code status line is a shell script that runs on every assistant message and prints whatever you want at the bottom of the interface. It receives a large JSON payload on stdin, roughly forty fields, and the interesting question is not how to configure it. It is which four things deserve a permanent row of your screen.

Most status lines end up showing the model name, the current directory, and a git branch, which is to say three things you already know. Here is what is actually in that payload, what is worth surfacing, and the two ways a status line quietly stops working.

Setup in thirty seconds

The fastest path is to describe what you want:

/statusline show model, context percentage, and a bar

Claude Code writes a script into ~/.claude/ and wires up your settings. The manual version is a statusLine block in ~/.claude/settings.json:

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

The command runs in a shell, so an inline one-liner works too. This one needs nothing but jq:

{
  "statusLine": {
    "type": "command",
    "command": "jq -r '\"[\\(.model.display_name)] \\(.context_window.used_percentage // 0)% context\"'"
  }
}

One reassurance before you start piling things on: the status line runs locally and costs no API tokens. Unlike the context rent that plugins charge, this is free real estate.

What actually earns a row

Four candidates, ranked by how often they change a decision you make.

Rate limit burn. rate_limits.five_hour.used_percentage and rate_limits.seven_day.used_percentage, with resets_at as Unix epoch seconds for each. This is the field almost nobody displays and the one that changes behavior most: knowing you are at 80 percent of your five-hour window before you kick off a long agent run is the difference between finishing the task and getting cut off mid-refactor. Claude Code even re-runs your script when a rate-limit window in the data it last sent reaches its resets_at, so the number stays honest without a timer.

Context percentage. context_window.used_percentage is pre-calculated, which means no arithmetic on token counts. It tells you when to wrap up or /compact before the model starts losing the thread, and it pairs with exceeds_200k_tokens if you want a hard warning line.

Session identity. session_name, workspace.git_worktree, and workspace.repo.name. If you only ever run one session, skip these. If you run three, they are the difference between confident and confused.

Cost. cost.total_cost_usd, computed client-side at list price, resetting to zero on /clear. It is an estimate, not your bill, and it is still the fastest feedback loop on "was that prompt worth it".

Here is a script with all four, sized to a terminal:

#!/bin/bash
input=$(cat)
eval "$(printf '%s' "$input" | jq -r '
  @sh "MODEL=\(.model.display_name)
       PCT=\(.context_window.used_percentage // 0 | floor)
       COST=\(.cost.total_cost_usd // 0)
       RL=\(.rate_limits.five_hour.used_percentage // 0 | floor)
       NAME=\(.session_name // .workspace.repo.name // \"\")"')"

printf '%s' "[$MODEL] $NAME | ctx ${PCT}% | 5h ${RL}%"
printf ' | $%.2f\n' "$COST"

Note COLUMNS and LINES: Claude Code sets them before running your script, and they are the only way to know the terminal width, because your output is captured rather than attached to the terminal. tput cols will not work from in there.

The fields most people never look at

Beyond the four above, the payload carries a few things worth knowing exist:

Output supports multiple lines (each echo is a row), ANSI colors, and OSC 8 clickable links in terminals that support them, which makes pr.url genuinely clickable in iTerm2, Kitty, or WezTerm. FORCE_HYPERLINK=1 overrides detection when the terminal supports links but Claude Code did not notice.

The multi-session angle

The reason to care about session identity in the status line is that agent work goes wide before it goes deep. Two or three sessions, one per worktree, is the normal shape of a productive day, and every one of those terminals looks identical at a glance.

A status line fixes exactly half of that problem. It tells you, instantly and always, what you are looking at right now. It cannot tell you anything about the session you are not looking at, and the expensive failure in multi-session work is the session sitting on a permission prompt in a window you tabbed away from twenty minutes ago. That half needs hooks and notifications, or something watching all of them at once. Status line for the foreground, notifications for the background: they are not competing answers.

The performance trap

Your script runs when a session starts, on every new assistant message, after /compact, when the permission mode changes, when vim mode toggles, and when you edit the command itself. That is a lot, and Claude Code debounces at 300ms.

Two consequences that bite:

  1. A slow script makes the status line stale, because updates wait for it to finish.
  2. If a new update fires while your script is still running, the in-flight script is cancelled. A git status in a large repository is slow enough to hit this.

The fix is caching. Write git information to a temp file with a timestamp, refresh it only every few seconds, and read the cache on every other invocation. It is fifteen extra lines and it is the difference between a status line that keeps up and one that flickers.

The opposite problem exists too: when the main session is idle, for example while a lead waits on background teammates, the event triggers go quiet and anything time-based freezes. Set refreshInterval (minimum 1 second) to also run on a timer.

When it goes blank

In rough order of likelihood:

  1. Workspace trust not accepted. Because statusLine runs a shell command, it is gated by the same trust rule as hooks in settings files. Until you accept the folder's trust dialog, the status line stays blank and claude --debug logs Status line command skipped: workspace trust not accepted.
  2. Non-zero exit or no output. Either one blanks the line. Always print something, even a fallback.
  3. Not executable, or writing to stderr. chmod +x, and check you are printing to stdout.
  4. Null fields before the first API response. context_window and cost are empty at session start, so use // 0 fallbacks in jq or you will render null.
  5. Windows path backslashes. Git Bash consumes them as escapes. Use forward slashes in the command path.
  6. Organization policy. disableAllHooks outside managed settings, or allowManagedHooksOnly, will remove your custom status line without warning.

Test without launching a session at all:

echo '{"model":{"display_name":"Opus"},"workspace":{"current_dir":"/x/proj"},"context_window":{"used_percentage":25}}' | ~/.claude/statusline.sh

If that prints and the real thing does not, the problem is the wiring, not the script.

The short version

  1. /statusline <describe it> writes the script for you. Manual config is a statusLine block with type: "command".
  2. Show rate limit burn, context percentage, session identity, and cost. Skip the things you already know.
  3. It costs no tokens, but a slow script costs freshness. Cache anything touching git.
  4. refreshInterval for anything time-based, since event triggers stop while the session is idle.
  5. Blank status line means trust, exit code, or a null field, in that order.
  6. It covers the session in front of you. Notifications cover the ones behind you.
Unwait does this for you

A macOS menu bar app that watches your Claude Code and Codex sessions, shows a short card while they work, and puts a strip on screen the moment one finishes. Free for two weeks, no card and no sign up.

Try for free
← All posts