Ramiz RajaBook a call
← Writing
Working With AI Agents

Claude Code Statusline: A Setup That Shows Your Limits

How the statusLine setting works, which fields the script receives, which settings file to put it in, and the small script I run every day.

Ramiz RajaOctober 26, 20265 min read
Claude Code Statusline: A Setup That Shows Your Limits

If you keep opening a menu or asking Claude Code how much context or usage you have left, a Claude Code statusline puts those numbers on screen all the time. It is a shell command you register in the statusLine setting: Claude Code pipes session data to it as JSON, and whatever it prints becomes the line under the prompt. Mine shows the branch, model, context used, and how much of my five-hour and seven-day usage windows is gone, with a countdown to each reset. I have no logged case where it changed a decision, so I don't claim one.

Updated 2026-10-26. Docs quoted: status line and settings, both fetched 2026-10-26.

How a Claude Code statusline is configured

The quick route is the /statusline command: give it a description in plain language and, per the docs, it generates a script in ~/.claude/ and updates your settings. The manual route is a statusLine object in a settings file:

{
  "statusLine": {
    "type": "command",
    "command": "~/.claude/statusline.sh",
    "refreshInterval": 30
  }
}

command is a script path or an inline shell command. padding is optional. refreshInterval re-runs the command every N seconds, minimum 1, on top of the event-driven updates. The docs suggest it "when your status line shows time-based data such as a clock", and I use 30 because mine shows a reset countdown.

Without it, the script runs on events: at session start, after a new assistant message, when /compact finishes, on a permission mode change, and a few others. Updates are debounced at 300ms (a change to the command itself skips the debounce), and if a new update triggers while your script is still running, Claude Code cancels the in-flight script.

Two behaviours worth knowing before you debug:

  • The docs say the status line "runs locally and does not consume API tokens". It hides temporarily during the help menu and permission prompts.
  • Output only shows if the script exits with code 0. A non-zero exit or empty output leaves the line blank.

Fields your script receives

The script gets one JSON object on stdin. These are the fields I use or checked, from the docs' list:

FieldWhat it holdsCaveat from the docs
model.display_nameModel name
effort.levelEffort levelAbsent when the current model does not support the effort parameter
cwd, workspace.current_dirWorking directory
context_window.used_percentageShare of the context window usedCounts input tokens only; may be null early in a session
rate_limits.five_hour, rate_limits.seven_dayused_percentage and resets_at (epoch seconds)Only for claude.ai Pro and Max subscribers or behind a Claude apps gateway that sets a spend limit, only after the first API response, and each window can be absent
session_idSession identifierUse it, not $$, to key any cache file

The rate_limits caveat decides how the script must be written: every window can be missing, so read with jq -r '... // empty' and print something sensible when it is. For what the limits themselves are, see Claude Code pricing.

My script, trimmed

This is my status line with the profile tag removed and the layout tidied. I ran it against mock JSON and it prints ! [no-git] proj | Sonnet 5.5/medium | ctx:42% | 5h:74%(r1h30m) | 7d:31%(r52h46m) for a session at 74% of the five-hour window. My live version prints the seven-day countdown as days and hours; this one keeps hours.

#!/bin/bash
input=$(cat)
cwd=$(echo "$input" | jq -r '.cwd // empty')
branch=$(git -C "${cwd:-.}" --no-optional-locks rev-parse --abbrev-ref HEAD 2>/dev/null)
mdl=$(echo "$input" | jq -r '.model.display_name // empty')
eff=$(echo "$input" | jq -r '.effort.level // empty')
prefix="[${branch:-no-git}] $(basename "${cwd:-.}") | ${mdl}${eff:+/$eff}"

ctx=$(echo "$input" | jq -r '.context_window.used_percentage // empty')
fh=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty')
fh_reset=$(echo "$input" | jq -r '.rate_limits.five_hour.resets_at // empty')
sd=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty')
sd_reset=$(echo "$input" | jq -r '.rate_limits.seven_day.resets_at // empty')

# No usage fields yet: short prefix only.
if [ -z "$ctx" ] || [ -z "$fh" ]; then printf "%s" "$prefix"; exit 0; fi

now=$(date +%s)
left() { # epoch -> "XhYm", or "done" if absent or already past
  [ -n "$1" ] && [ "$1" -gt "$now" ] || { echo done; return; }
  d=$(( $1 - now )); echo "$(( d / 3600 ))h$(( d % 3600 / 60 ))m"
}
warn=""; [ "$(printf '%.0f' "$fh")" -ge 70 ] && warn="! "
week=""; [ -n "$sd" ] && week=$(printf " | 7d:%.0f%%(r%s)" "$sd" "$(left "$sd_reset")")
printf "%s%s | ctx:%.0f%% | 5h:%.0f%%(r%s)%s" \
  "$warn" "$prefix" "$ctx" "$fh" "$(left "$fh_reset")" "$week"

Three choices in it, and why:

  • The fallback. When context_window.used_percentage or the five-hour figure is missing, it prints only the branch, directory and model. Early in a session, or without a subscription, a missing usage field doesn't blank the line.
  • The ! at 70%. The threshold is mine, not the docs'. It compares the rounded figure the line displays, so 69.6 shows as 70% with the mark. It is a prompt to look at the countdown before I start a long run, not a rule.
  • The git call. It runs on every refresh. The docs recommend caching slow operations in a temp file keyed by session_id; mine does not cache it, so a big repo may need that.

The script needs jq installed. Test it the way the docs say to, by piping mock input into it before you register it: echo '{"cwd":"/tmp","model":{"display_name":"Sonnet 5.5"}}' | ~/.claude/statusline.sh should print the short prefix, and adding context_window and rate_limits objects should print the full line. Save it as ~/.claude/statusline.sh and chmod +x it. If the line stays blank, check the script is executable and exits 0. The docs also note the status line stays blank until you accept the workspace trust dialog for the folder, because it executes a shell command.

Claude Code settings: which settings.json holds it

The settings docs list four places a setting can live: user (~/.claude/settings.json), shared project (.claude/settings.json), project-local (.claude/settings.local.json) and managed. From highest precedence down: managed, command-line --settings, project-local, shared project, user. List settings such as permissions.allow merge across files rather than replace each other.

Mine is in user settings, so it applies in every project. Settings files are strict JSON, so no comments or trailing commas, and a stray comma will stop the file loading. /status shows which settings sources loaded, and most edits apply without a restart.

For what else belongs in the project layer, see what goes in CLAUDE.md. If you want a command to run on events rather than draw a line, that is a hook: Claude Code hooks.

What I did not measure

I haven't compared status line tools, and the docs name community projects I haven't run. I have no data on how often the ! warning led me to wait.

Related: Claude Code Compact: What Survives, With Counts From My Sessions and Claude Code YOLO Mode: What It Removes and What I Run Instead.