Recipe · Basic · 10 min
Claude Code statusline: live model, context and usage limits
A dependency-free Python status line for Claude Code: model, branch, colored context, 5-hour and 7-day limits, and what to do when it doesn't show.
Tested with Claude Code 2.1.316 · reviewed
See the files on GitHub Free · MIT license
The problem
While you work with Claude Code you don't know how much context you have left, which branch you're on or how much of your 5-hour and 7-day limits you've used until it's too late. The statusline solves it: a row at the bottom that runs your script. But many example scripts use jq, which doesn't come installed on Ubuntu (we checked), and then the bar shows up empty without you knowing why.
The solution
The status line is a customizable row at the bottom of Claude Code, above the footer indicators. Claude Code runs the command you configure, passes it a JSON with session data on standard input (model, folder, context, estimated cost and, on Pro, Max and Ultra plans, usage of the 5-hour and 7-day limits) and shows whatever the script prints to standard output. It runs locally and uses no tokens.
1. The quick way: /statusline
If you don't want to write anything, use the /statusline command with what you want to see in plain language:
/statusline show the model, the git branch and the context percentage with a progress bar
Claude Code generates a script in ~/.claude/ and updates your settings; approve the edits if it asks. With no arguments, it tries to set it up from your shell prompt. To remove it, /statusline delete (or "clear", "remove it"), or delete the statusLine key from your settings.json by hand.
The quick way is fine to start, but it pays to know what's inside: if the generated script uses jq and you don't have it installed, the bar comes out empty or garbled. That's why this recipe uses Python, which ships with almost every Linux and macOS. If you prefer jq, install it first (sudo apt install jq on Ubuntu or Debian).
2. The script (Python, no dependencies)
Save it as ~/.claude/statusline.py:
#!/usr/bin/env python3
# Save it as ~/.claude/statusline.py and make it executable: chmod +x ~/.claude/statusline.py
"""Status line: model · folder (branch) · context · 5 h / 7 d limits · cost."""
import json, os, subprocess, sys
d = json.load(sys.stdin)
model = (d.get("model") or {}).get("display_name", "?")
cwd = (d.get("workspace") or {}).get("current_dir") or d.get("cwd", "")
pct = int((d.get("context_window") or {}).get("used_percentage") or 0)
cost = (d.get("cost") or {}).get("total_cost_usd") or 0
limits = d.get("rate_limits") or {}
try:
branch = subprocess.run(["git", "-C", cwd, "branch", "--show-current"],
capture_output=True, text=True, timeout=1).stdout.strip()
except Exception:
branch = ""
color = "\033[31m" if pct >= 80 else "\033[33m" if pct >= 50 else "\033[32m"
parts = [f"[{model}] {os.path.basename(cwd)}" + (f" ({branch})" if branch else ""),
f"{color}ctx {pct}%\033[0m"]
for key, label in (("five_hour", "5h"), ("seven_day", "7d")):
used = (limits.get(key) or {}).get("used_percentage")
if used is not None:
parts.append(f"{label} {used:.0f}%")
parts.append(f"${cost:.2f}")
print(" | ".join(parts))
It shows the model in brackets, the folder and git branch, the context percentage in color (green below 50 %, yellow up to 80 %, red from 80 %), usage of the 5-hour and 7-day limits if present, and the session's estimated cost. It uses the official fields:
| Field | What it is |
|---|---|
model.display_name | Model name |
workspace.current_dir | Working folder |
context_window.used_percentage | Percentage of context used |
rate_limits.five_hour.used_percentage | Usage of the 5-hour limit |
rate_limits.seven_day.used_percentage | Usage of the 7-day limit |
cost.total_cost_usd | Session's estimated cost |
Two important details from the docs: rate_limits only exists for Pro, Max and Ultra subscribers (or behind a gateway with a spend limit) and only after the session's first response; and used_percentage can be null at the start. That's why the script uses defaults and never fails when a field is missing. The cost is a list-price estimate computed on your machine: it may not match your bill and resets to 0 with /clear.
3. Turn it on in settings.json
Make the script executable and add the statusLine block to ~/.claude/settings.json:
chmod +x ~/.claude/statusline.py
{
"statusLine": {
"type": "command",
"command": "~/.claude/statusline.py"
}
}
It also works in the project settings, but then the whole team inherits it. Claude Code reloads the settings by itself and runs the script when you save. If you change the script, the change shows on the next event that runs it again (for example, the next message). For data that changes over time, like a clock, add "refreshInterval": 5 inside statusLine and it will also run every 5 seconds (minimum 1).
If you prefer jq, this is the one-line alternative from the official docs (requires jq installed):
{
"statusLine": {
"type": "command",
"command": "jq -r '\"[\\(.model.display_name)] \\(.context_window.used_percentage // 0)% context\"'"
}
}
4. Extras worth having
- Multiple lines: each line you print is a row.
- Colors and links: ANSI colors (
\033[32mis green) and clickable OSC 8 links in terminals that support them. - Width: inside the script
tput colsdoesn't work because Claude Code captures the output; use theCOLUMNSandLINESvariables, which Claude Code sets before running it. - Prompt cache: the
prompt_cacheobject haswarm(whether the cache is still alive) andhit_ratio; showing "cache cold" warns you that the next message will cost more. - Slow operations: if you show
git statusin a big repo, cache the result for a few seconds in a temp file with thesession_idin its name (don't use$$, it changes on every run). - Subagents:
subagentStatusLinelets you customize each subagent's row in the agents panel. - Something ready-made: the docs mention community projects such as ccstatusline and starship-claude.
Check it
Before looking at the bar, test the script outside Claude Code with mock data. If it prints a line and exits with code 0, it will work in Claude Code. Also try a nearly empty JSON to check it copes with missing or null fields:
# 1. Test the script outside Claude Code with mock data
echo '{"model":{"display_name":"Opus"},"workspace":{"current_dir":"'$PWD'"},"context_window":{"used_percentage":62.4},"cost":{"total_cost_usd":0.43},"rate_limits":{"five_hour":{"used_percentage":23.5},"seven_day":{"used_percentage":41.2}},"session_id":"test"}' | ~/.claude/statusline.py
# [Opus] my-project (main) | ctx 62% | 5h 24% | 7d 41% | $0.43
# 2. No limits or data (session start or API key): it must not break
echo '{"model":{"display_name":"Sonnet"},"workspace":{"current_dir":"/tmp"},"context_window":{"used_percentage":null},"session_id":"x"}' | ~/.claude/statusline.py; echo "exit=$?"
# [Sonnet] tmp | ctx 0% | $0.00
# exit=0
We tested it on 1 Nov 2026 on Linux with Claude Code 2.1.316 and Python 3.12.3, without touching the user's settings (using claude --settings <file>): the script passes both tests and, in a trusted folder, the bar rendered like this:
[Opus 5.5] proyecto-50usd (main) | ctx 0% | $0.00
We didn't get to see rate_limits in the real bar, because it only arrives after the first response and we sent no message in that test.
Why it works
The script runs when a session starts or resumes, and again when a new assistant message arrives, when /compact finishes, when you change permission mode or vim mode, when you change the command and when a limit window reaches its reset time. Updates are batched with a 300 ms debounce and, if another one arrives while your script is still running, the previous run is cancelled. Note: with a custom bar, Claude Code stops showing most of the footer's keyboard hints, such as "esc to interrupt", and the bar hides for a moment during some dialogs, for example when it asks for permission.
Why does /context show more than your bar? It's not a bug. The bar shows the counts from the last API response, while /context adds an estimate of messages that arrived afterwards, so /context can show more until the next response. context_window.used_percentage is computed from input tokens only (regular input plus cache writes and reads), without output; if you compute the percentage yourself from current_usage, use the same formula, and keep in mind that current_usage is null before the first call and right after /compact. Another useful field: exceeds_200k_tokens says whether the last response went over 200,000 tokens, a fixed threshold even if your model has a 1-million window; context_window.context_window_size gives you the real window size. It's all in the statusline documentation.
Common mistakes
If your statusline doesn't show, check in this order:
- Blank bar in a new folder → you didn't accept workspace trust: since the bar runs a shell command, it follows the same rule as hooks and stays blank until you accept the "Is this a project you created or one you trust?" dialog for that folder (or a parent) → restart Claude Code and accept the dialog.
- Nothing shows → the script isn't executable →
chmod +x ~/.claude/statusline.py. - It prints but nothing shows → it writes to stderr → the bar only shows what goes to stdout.
- Empty bar after an error → the script exited with a non-zero code or printed nothing → test it with the mock data from "Check it".
- The log says
jq: not found→ the command usesjqand it isn't installed (it happened on our PC) → install it or use the Python script. - It disappeared without warning →
disableAllHooks: trueoutside managed settings turns the bar off, and your organization'sallowManagedHooksOnlymakes it disappear → check your settings. - It doesn't work on Windows with Git Bash → the command path uses
\backslashes → use/forward slashes.
To see what's going on, start with debug logging. It records your script's stderr and exit code:
# Still blank? Start with debug logging and look for the bar
claude --debug-file /tmp/claude-debug.log
# (in another terminal)
grep -i "statusline\|status line" /tmp/claude-debug.log
We saw these lines in our tests, the first with the Python script and the other two with a command that used jq without it being installed:
[DEBUG] StatusLine [~/.claude/statusline.py] completed with status 0
[DEBUG] StatusLine [jq -r ...] stderr: /bin/sh: 1: jq: not found
[WARN] StatusLine [jq -r ...] completed with status 1
If the problem is workspace trust, according to the official docs the log says Status line command skipped: workspace trust not accepted. We didn't verify that message ourselves: we didn't accept the trust dialog so as not to modify the user's configuration.
FAQ
Does the statusline use tokens or usage limits?
No. It runs on your machine with data Claude Code already has; it makes no API calls.
Why don't I see the 5-hour and 7-day usage?
The rate_limits object only reaches Pro, Max and Ultra subscribers (or behind a gateway with a spend limit) and only after the session's first response. With an API key it doesn't appear. Each window can be missing separately.
Is the cost it shows what I pay?
Not necessarily. cost.total_cost_usd is a list-price estimate computed on your machine; the docs warn it may differ from your actual bill, and on subscriptions it's not what you're charged. It resets to 0 with /clear.
It shows "--" or empty values, is it broken?
At the start of the session some fields arrive as null. Use defaults (in Python or 0, in jq // 0). If they're still empty after several messages, restart Claude Code.
Does it work on Windows?
Yes. Claude Code runs the command with Git Bash if it's installed, or with PowerShell if not. Write paths with / slashes, or call a script with powershell -NoProfile -File C:/Users/your-user/.claude/statusline.ps1.
Should I put it in the project or in my user settings?
In ~/.claude/settings.json if it's yours. In the project's .claude/settings.json the team inherits it, and it only runs once each person accepts workspace trust for the folder. To see which other pieces are configured there, check which piece to use.