Max Narditdata & ai systems
← Articles
7 mindev

Claude Code statusline and ccusage: the native rate limits, and a 5-hour block that ends at a different time

Two tools answer one question with different evidence: the server reports a reset time, a log parser infers one. When they disagree by hours, believe the server and keep the parser for history. The dollar figure, on a subscription, is an exchange rate.

Contents

Claude Code’s status line used to be where you put the model name and a git branch. Since the rate_limits object landed on its stdin, it can also show a Pro or Max subscriber the two numbers that stop work: how much of the 5-hour window and of the 7-day window is gone, and when each one resets. Guides written before that point you to ccusage, which computes costs and its own 5-hour blocks from the JSONL transcripts on your disk. Those are different measurements, not the same numbers by another route.

Both are useful, but they are not measuring the same thing. I ran one interactive session with a statusline that logged everything Claude Code sent it, then fed that exact input to ccusage statusline. Claude Code said the 5-hour window resets at 04:30 UTC. ccusage said its 5-hour block had 3 hours and 1 minute left, at 03:58 UTC. A gap of about two and a half hours, on the same machine, in the same minute. What follows is what each field covers, the script I used, and why the two can disagree.

What usage data does the Claude Code statusline receive?

A JSON object on stdin, every time the line refreshes, with four groups that matter for usage: rate_limits, context_window, cost and prompt_cache. The first is the only one that describes your plan. The others describe the current session.

Per the statusline docs, rate_limits.five_hour and rate_limits.seven_day each carry used_percentage (0 to 100) and resets_at in Unix epoch seconds. In my session they arrived as whole numbers. context_window gives used_percentage of the model’s window, computed from input tokens only, plus context_window_size, which was 1,000,000 for Haiku 5.5 here, so “7%” meant about 65,000 tokens. cost.total_cost_usd is a session estimate. The prompt_cache object reports hit ratio, misses and whether the cache is warm; it is the same data /usage shows, and it is the field to watch if you are chasing what broke your prompt cache.

The line runs at session start and re-runs, among other triggers, when a new assistant message arrives, after /compact, when a rate_limits window in the last input reaches its resets_at, when a warm prompt cache reaches its expires_at, and on every refreshInterval tick if you set one. Updates are debounced at 300 ms. One extra key showed up on my 2.1.302 stdin that the docs table doesn’t list, scratchpad_dir; don’t build on it.

When are Claude Code’s rate_limits fields missing?

On the first run of every session, for anyone who is neither on a subscription nor behind a spend-limited gateway, and, window by window, once a reset time has passed. The docs say rate_limits appears “only for claude.ai Pro and Max subscribers, or behind a Claude apps gateway that sets a spend limit for you, and only after the first API response in the session,” and that Claude Code drops a window once its resets_at passes.

The startup run in my log matched that exactly:

first run:   rate_limits absent, prompt_cache absent, context_window.used_percentage null, cost 0
after reply: rate_limits.five_hour 4% (resets 04:30 UTC), seven_day 45%, context 6%

So a script that indexes straight into rate_limits.five_hour crashes on startup, and a blank status line is all you see, because Claude Code shows output only after the script exits 0. Every documented field that can be absent or null needs a fallback.

How do you build a Claude Code statusline from the native fields?

Read stdin once, print one line, and treat the usage fields as optional. The docs’ rate-limit example prints the two percentages. Mine, in Python 3 with only the standard library, adds the time left in each window and the context figure, which is what the rest of this article is about:

python
#!/usr/bin/env python3
import json, sys, time
 
data = json.load(sys.stdin)
parts = [(data.get("model") or {}).get("display_name", "?")]
 
ctx = (data.get("context_window") or {}).get("used_percentage")
parts.append(f"ctx {ctx:.0f}%" if ctx is not None else "ctx --")
 
now = time.time()
for key, label in (("five_hour", "5h"), ("seven_day", "7d")):
    win = (data.get("rate_limits") or {}).get(key) or {}
    pct, reset = win.get("used_percentage"), win.get("resets_at")
    if pct is None or reset is None or reset <= now:
        continue
    left = int(reset - now)
    if left >= 86400:
        eta = f"{left // 86400}d{left % 86400 // 3600}h"
    else:
        eta = f"{left // 3600}h{left % 3600 // 60:02d}m"
    parts.append(f"{label} {pct:.0f}% ({eta})")
 
print(" | ".join(parts))

Save it as ~/.claude/statusline.py, chmod +x it, and merge this into ~/.claude/settings.json:

json
{
  "statusLine": {
    "type": "command",
    "command": "~/.claude/statusline.py",
    "refreshInterval": 60
  }
}

refreshInterval is there for the countdown. Without it the line only re-runs on events, and an idle session keeps showing the time left as of its last message. On the first and the last of the five payloads I logged, replayed at 04:07 UTC, the script printed:

Haiku 5.5 | ctx --
Haiku 5.5 | ctx 7% | 5h 7% (0h22m) | 7d 45% (3d16h)

Test yours the same way before you trust it. For one session, set command to tee -a ~/.claude/statusline-input.jsonl | ~/.claude/statusline.py, send a couple of messages, put the plain command back, and replay any line with tail -1 ~/.claude/statusline-input.jsonl | ~/.claude/statusline.py. The skipped expired windows matter here, since a replayed payload’s reset times are in the past. One more thing from the docs: in a folder you haven’t trusted, the status line stays blank until you accept the trust dialog, because it runs a shell command under the same rule as hooks.

What does ccusage statusline add to Claude Code’s own fields?

History. ccusage reads the local transcripts and adds three numbers Claude Code doesn’t put on stdin: today’s total across sessions, the cost of the current 5-hour block with its time left, and a burn rate. At 03:58 UTC, with --cost-source both, its line read (dollar figures for the day and block replaced):

🤖 Haiku 5.5 (medium) | 💰 ($0.01 cc / $0.01 ccusage) session / $X today / $Y block (3h 1m left) | 🔥 $Z/hr | 🧠 64,531 (6%)

The session figure is not ccusage’s own by default. With --cost-source auto, the default, it shows Claude Code’s cost.total_cost_usd and only computes its own when that field is absent; ccusage forces its calculation and both prints the two side by side. Pricing comes from a cached LiteLLM table (offline is the default), and output caching is on by default with a one-second cache expiry, which is unrelated to Claude Code’s refreshInterval. The page marks the feature as beta. I ran it as bun x ccusage@20.0.35 statusline; npx -y ccusage statusline is the documented alternative.

What ccusage does not show at all is the 7-day percentage. There is nothing in local logs that tells it your weekly allowance, so it has no denominator for one.

Does ccusage’s 5-hour block match Claude Code’s five_hour window?

Not in my check. The two disagreed on when the window ends, which is the part a subscriber plans around.

03:58 UTC   Claude Code  rate_limits.five_hour.resets_at = 04:30 UTC  (32 min left)
03:58 UTC   ccusage      block: 1h 58m elapsed, 3h 1m remaining       (ends about 07:00 UTC)

In this run the ccusage block started at 02:00, nearly two hours before my test session existed, so it was already counting other activity on this machine. The blocks report docs describe a block as “Triggered by your first message” that “Lasts exactly 5 hours from start time,” and claim it:

Matches Claude's internal session tracking

I didn’t trace why they diverged, and my test can’t separate two candidates. One: with steady use, a chain of 5-hour windows anchored at one moment and a chain anchored at another stay offset forever, even if both see every request. Two: ccusage sees only the transcripts in its data directories, while the server meters the account’s usage from other devices and from claude.ai as well. Either way, in this run the block’s dollar figure covered a different five hours from the ones Anthropic was metering, and its “time left” was off by two and a half hours. One check on one machine doesn’t tell you how often that happens. It does tell you which number to believe when they disagree: resets_at is the server’s own answer.

Do Claude Code’s session cost and ccusage’s session cost agree?

Close, not equal, and for a subscriber neither is a bill. For the same session Claude Code reported total_cost_usd 0.01257 and ccusage session computed 0.01149 from the transcript, about 9% lower. Both rendered as $0.01 on the line, so --cost-source both showed no difference at all.

I didn’t trace the gap. Candidates are a different price table, or requests that never land in the transcript; the costs page mentions background requests such as prompt suggestions and conversation summarization. What the docs do settle is the meaning: Claude Code computes the figure “locally from token counts at list price,” and for Pro and Max subscribers “the session cost figure isn’t relevant for billing purposes.” On a subscription the dollar number is an exchange rate, API-equivalent spend, useful for comparing sessions or for knowing what a workload would cost on API credits, not for knowing when you will be cut off.

Which Claude Code usage numbers are account-wide and which are local?

Only rate_limits is account-wide. Everything else is narrower:

rate_limits.five_hour / seven_day   the account, as the server sees it
cost.total_cost_usd                  this session, list price, reset by /clear
context_window                       this session's last API response
ccusage today / block / burn rate    local transcripts, filtered to the day or block
/usage plan breakdown                local session history on this machine

The costs page says the /usage breakdown is “computed from local session history on this machine, so usage from other devices or claude.ai is not included,” and ccusage is local by construction. That matters most when you run many sessions at once. Their percentages all describe the same allowance; per-session cost and context tell you about one session and nothing about how close the account is to its limit. Anthropic’s help center says Pro and Max limits are shared between Claude and Claude Code, so chat usage moves rate_limits too, while neither ccusage nor the /usage breakdown sees it.

The native stdin also has no model-specific window. If your plan has a separate weekly Sonnet limit, the statusline can’t show it. That is the gap Claude Usage Deck covers in the browser, where it reads your claude.ai usage limits and shows the session, weekly and Sonnet weekly meters.

What should a Claude Code statusline show on a Pro or Max plan?

The two rate_limits percentages with their reset times, and context. Those are the numbers that change what you do next: whether to start a long task now or wait for 04:30, and when to /compact. Put the dollar figure on the line only if you pay per token, and even then it is an estimate, at list price or at your contracted rates if an admin set modelPricing.

Keep ccusage, but for what it is good at. ccusage claude weekly --last 1 --mode calculate answers “what did my Claude Code transcripts here burn this week at API prices” (weeks start on Sunday in your system timezone unless you pass --start-of-week and --timezone), which is a real question when you are deciding whether a workload belongs on a subscription at all. It is the same question that sat behind Anthropic’s plan to meter programmatic use separately. What a log can’t do is tell you where the server’s window ends. For that, the answer is already on stdin.

01 / Discussion

No comment section here — all discussions happen on X.

Max Nardit@mnardit

02 / More articles