cctally refresh-usage
June 13, 2026 · View on GitHub
Force-fetch 7-day and 5-hour rate-limits from the Anthropic OAuth usage API and
persist them via the same path record-usage uses.
Normally not needed because cctally hook-tick calls the same OAuth-fetch
path automatically. Manual invocation is mostly for force-refresh / debugging.
Difference from hook-tick
refresh-usage busts the status-line cache file
(/tmp/claude-statusline-usage-cache.json) on success, so the next status-line
tick re-fetches. hook-tick calls a non-busting variant of the same fetch path
to avoid interfering with the status-line cache.
User-Agent
cctally refresh-usage sends User-Agent: claude-code/<discovered-version> by
default. Anthropic gates the OAuth /usage endpoint behind a per-User-Agent
rate-limit; presenting the official Claude Code UA bypasses that gate so the
fetch returns 200 instead of 429 during active sessions.
The discovery chain (first hit wins):
- Active executable.
claude --version 2>/dev/null(5 s timeout), parsed against\d+\.\d+\.\d+(?:-[A-Za-z0-9.]+)?. - Versions directory glob.
~/.local/share/claude/versions/filtered to semver-shaped entries; highest semver wins. - Frozen sentinel. A pinned fallback version baked into the script.
The default can be overridden via the oauth_usage.user_agent config field —
see Honesty mode below.
Flags
--quiet— suppress text output--json— emit structured envelope--color {auto,always,never}— color discipline--timeout N— HTTP timeout (default 5s)
Exit codes
| Code | Meaning |
|---|---|
| 0 | Success or rate-limited (graceful fallback emitted) |
| 2 | No OAuth token (run claude once to authenticate) |
| 3 | Network / HTTP failure (non-429) |
| 4 | Malformed response |
| 5 | record-usage failure |
A 429 response from the OAuth usage API is not an error from the user's
perspective — there is no actionable retry without changing UA. The command
serves last-known data from weekly_usage_snapshots, prints a freshness
indicator, and exits 0. See the next section for the exact output shape.
Output: rate-limited mode
When the upstream API returns 429, refresh-usage falls back to the most
recent row in weekly_usage_snapshots (ordered by captured_at_utc DESC, id DESC) and exits 0 in all 429 cases.
Text mode (stderr). One line:
-
With prior snapshot:
refresh-usage: rate-limited; using last-known (captured Ns ago)Followed on stdout by the standard one-liner rendered from the fallback payload (with
src:db-fallback cache:absent). When the snapshot'sresets_at_epochisnull(rows captured before theweek_end_atmigration), the renderer drops the(in Xd Yh)TTL segment for that block. -
With no prior snapshot:
refresh-usage: rate-limited; no last-known data; status-line will populate on next CC tickNothing is printed on stdout.
JSON mode (--json). A rate-limited envelope replaces the success
payload:
{
"status": "rate_limited",
"fallback": {
"schema_version": 1,
"fetched_at": "<iso8601>",
"seven_day": {
"used_percent": 47.0,
"resets_at": "<iso8601>",
"resets_at_epoch": 1735689600
},
"five_hour": {
"used_percent": 12.0,
"resets_at": "<iso8601>",
"resets_at_epoch": 1735603200
},
"source": "db-fallback",
"statusline_cache": "absent"
},
"freshness": {
"label": "fresh",
"captured_at": "<iso8601>",
"age_seconds": 12
},
"reason": "user-agent rate-limit gate"
}
Notes:
freshness.labelis one offresh/aging/stale, derived fromoauth_usage.fresh_threshold_seconds(default 30) andoauth_usage.stale_after_seconds(default 90).fallback.sourceis"db-fallback"(not"oauth") so consumers can distinguish fallback data from a fresh fetch.fallback.five_houris omitted (set tonull) when the latest snapshot has no recorded 5-hour block.fallback.seven_day.resets_at_epochmay benullfor snapshots predating theweek_end_atmigration; the same applies tofallback.five_hour.resets_at_epoch. The text renderer drops the(in Xd Yh)segment whenever a block'sresets_at_epochisnull.
When no prior snapshot exists, fallback is null, freshness is null,
and reason is "no prior snapshot". Exit is still 0.
Honesty mode
Users who do not want cctally to impersonate Claude Code can set an
explicit User-Agent in ~/.local/share/cctally/config.json:
{
"oauth_usage": {
"user_agent": "cctally/0.1",
"fresh_threshold_seconds": 30,
"stale_after_seconds": 90
}
}
When oauth_usage.user_agent is a non-empty string, that exact value is
sent in the User-Agent header — the claude-code/<discovered>
impersonation default is disabled.
Consequence. Anthropic's per-UA rate-limit gate will respond 429 Too Many Requests to any UA other than claude-code/* during active sessions.
refresh-usage then takes the rate-limited path described above: last-known
data from the DB, a freshness label derived from
fresh_threshold_seconds / stale_after_seconds, and exit 0. The
status-line and dashboard chips will reflect the staleness instead of
showing a hard error.
This is the recommended posture for users with stricter compliance requirements who would rather see slightly stale data than send the official Claude Code UA from a non-Claude-Code process.
To revert to the impersonation default, set user_agent back to null or
remove the field.
Propagation model
refresh-usage updates the underlying data synchronously: it writes weekly_usage_snapshots, advances the hwm-7d high-water mark, and busts the external statusline cache (/tmp/claude-statusline-usage-cache.json) before it returns. The live views catch up on their own cadences:
- Dashboard — on a successful refresh,
refresh-usagesends a best-effortPOST /api/sync?refresh=0to a dashboard on127.0.0.1:8789, which re-reads the DB and broadcasts the new value over SSE ~instantly. The nudge is fire-and-forget: if no dashboard is listening (or it runs on a non-default port), the call fails silently and that dashboard self-heals within its--sync-interval(default 5s). Exit codes, stdout, and stderr are unchanged whether or not a dashboard is running. - Claude Code terminal status line (the
5h X% · 7d Y%chip) — repaints only when Claude Code next re-invokescctally statusline, on Claude Code's own cadence. cctally cannot force Claude Code to re-render, so this surface may briefly lag the valuerefresh-usageprinted.
See also
hook-tick— automatic per-fire variant (no cache-bust)setup— installs the hooks that drivehook-tickrecord-usage— receiver for both fetch paths and the opt-in status-line integration