Skip to main content

Where Claude Code's Token Usage Actually Lives

· 12 min read
Pere Pages
Software Engineer
Two stacked file cards on a dark desk: a tall append-only log of repeated identical lines, and beside it a single small card stamped with a clock, connected by a thin dotted line to a distant server

Claude Code writes two records to your disk: an append-only ledger of every exchange with the model, and a small cached snapshot of how full your rate limits are. Almost every tool and blog post about local usage tracking knows about the first one and misses the second — including, until I actually looked, me.

note

Figures here are a snapshot from August 2026 — star counts, download numbers, file layouts and the tool landscape all move fast. The ~/.claude.json shape below is undocumented and could change without notice. Re-check anything you plan to rely on.

That split matters more than it sounds, because it decides the answer to a question a lot of people reach independently: should I build a little menu bar widget to watch my usage? The honest answer changes once you know both files exist.

The ledger: one line per exchange

Claude Code writes one file per session in the JSON Lines format (JSONL) — one self-contained JavaScript Object Notation (JSON) object per line, newline-delimited:

~/.claude/projects/<project-slug>/<session-id>.jsonl

The project slug is your working directory with the path separators flattened into dashes, so a session run in ~/Projects/token-usage lands under -Users-you-Projects-token-usage. Each assistant message in that file carries a usage object, verbatim from the application programming interface (API) response:

{
"input_tokens": 2,
"cache_creation_input_tokens": 17513,
"cache_read_input_tokens": 20554,
"output_tokens": 895,
"server_tool_use": { "web_search_requests": 0, "web_fetch_requests": 0 }
}

Four token counters (plus a tally of server-side tool calls), and the difference between the four is most of the story:

  • input_tokens — fresh tokens sent up this turn that weren't already cached. Often tiny.
  • cache_creation_input_tokens — tokens written into the prompt cache. You pay a premium to put them there, once.
  • cache_read_input_tokens — tokens served from that cache. These bill at a fraction of the fresh-input rate, and in a long agentic session they utterly dominate the volume.
  • output_tokens — what the model generated. The expensive ones, per token.

The example above is typical of a working session: 20,554 tokens read from cache, against 2 fresh input tokens. A naive widget that adds all four counters together and calls the result "tokens used" will produce a number that is enormous, technically accurate, and almost completely disconnected from what the turn cost you. Any tool worth using has to weight them separately.

So far this is the well-trodden part. Every local usage tool starts here, and the usual next sentence is that your actual limit consumption is not on disk — that you can only reconstruct it by grouping these timestamps into rolling five-hour blocks and guessing. That sentence is wrong, and it's wrong for a slightly comic reason.

The file beside the directory

The rate-limit state is cached locally. It lives in ~/.claude.json — which is not inside ~/.claude/, but sitting next to it. A recursive grep of the ~/.claude tree for resets_at or weekly_limit comes up empty, and that empty result is where the folklore comes from. The file everyone is looking for is one directory level up, hidden in plain sight.

Inside it is a cachedUsageUtilization key, and it holds exactly the numbers a widget wants:

{
"fetchedAtMs": 1786284230391,
"utilization": {
"five_hour": { "utilization": 0, "resets_at": "2026-08-09T18:50:00Z", "limit_dollars": null },
"seven_day": { "utilization": 12, "resets_at": "2026-08-12T09:00:00Z", "limit_dollars": null },
"limits": [
{ "kind": "session", "percent": 0, "severity": "normal", "resets_at": "2026-08-09T18:50:00Z" },
{ "kind": "weekly_all", "percent": 12, "severity": "normal", "resets_at": "2026-08-12T09:00:00Z" }
]
}
}

That is the real server-side utilization, as a percentage, with genuine reset timestamps — not a reconstruction. The same file carries a per-project lastModelUsage map, broken down by model, with a cost figure Claude Code computed for you:

"/Users/you/Projects/some-repo": {
"lastModelUsage": {
"claude-sonnet-4-6": {
"inputTokens": 69,
"outputTokens": 15276,
"cacheReadInputTokens": 988583,
"cacheCreationInputTokens": 65172,
"webSearchRequests": 0,
"costUSD": 1.2838614999999998
}
}
}

Per-repository attribution, already computed, sitting on disk. Which is a much better starting position than the folklore suggests.

Cached is not authoritative

Here's the catch, and it's the interesting one — a good deal more interesting than "the data isn't there."

That fetchedAtMs field is not decoration. cachedUsageUtilization is a snapshot of a server-side value at a moment in the past, refreshed when Claude Code has reason to talk to the server. Nothing on your machine updates it as you burn through a session, and nothing guarantees it is fresh when you read it. A widget polling that file every two seconds is polling a value that may not have moved in an hour, and it has no way to tell the difference between "your usage genuinely hasn't changed" and "this number is stale." Any honest reading of it has to be rendered alongside its age.

Three smaller caveats follow from the same shape.

  • The percentage is all you get. limit_dollars, used_dollars and remaining_dollars are all null on a subscription plan. You can render a bar; you cannot render a balance.
  • It only knows about Claude Code, on this machine. Conversations in the web app, the desktop app, a second laptop, or raw API scripts leave no trace in either file. A "total usage" headline drawn from them is really "usage from one client on one computer" — even though the utilization percentage itself is account-wide, which makes the mismatch between the two numbers genuinely confusing to display side by side.
  • costUSD is arithmetic, not a statement you received. It's token counts multiplied by a price table. On a subscription plan that figure answers "what would this have cost on pay-as-you-go", which is interesting, but it is not money that moved.

None of this makes the local data useless. It makes it specific: excellent for history and attribution, adequate-with-an-asterisk for "am I close to my limit right now."

Parsing it is nearly free

Given all that, you might expect the engineering to be the hard part. It isn't — and it's worth knowing how not-hard, because it removes performance from the list of reasons to prefer one approach over another.

A full cold scan of my own corpus — 320 MB across 113 session files, 36,960 lines — completes in 0.34 seconds in plain Python, with zero unparseable lines. Not a compiled language. Not a database. A for loop.

The usual advice is to add a cheap substring guard before committing to a parse:

for line in file:
if '"usage"' not in line: # substring test, no parsing
continue
record = json.loads(line)

It does help — but less than you'd think, and it's worth being honest about the size of the win. On the same corpus, parsing every line unconditionally takes 0.46 seconds against the guarded loop's 0.34. That's a 1.35× speedup, not an order of magnitude. The real headline isn't the trick; it's that the naive version was already half a second.

Where the guard genuinely pays off is that these files are append-only in practice, so a long-running process never needs the cold scan twice: remember the byte offset where you stopped, seek there next time, and read only what's new. Steady-state cost rounds to zero.

Which means the choice between polling every two seconds and subscribing to filesystem events is close to irrelevant. Poll. As established above, the number you're polling might not have changed in an hour anyway.

What already exists

Two mature tools already read this data, and they sit at opposite ends of the design space.

ccusage is a command-line interface (CLI) — the de-facto one, at around 17.8k GitHub stars and 87,009 downloads in a single week of August 2026, with zero runtime dependencies. It parses the session files described above and reports daily, monthly and per-session breakdowns with cost estimates, plus a blocks command that groups usage into five-hour billing windows. Worth flagging if you're following older write-ups: its live terminal user interface (TUI) monitor, blocks --live, was removed in v18.0.0 and the documentation now points at the statusline command for real-time tracking instead.

CodexBar takes the other route — a macOS 14+ menu bar app in Swift, under the Massachusetts Institute of Technology (MIT) licence, installable with brew install --cask codexbar. Rather than relying on one vendor's cache file, it reuses whatever session you already have — Open Authorization (OAuth) tokens, device flows, API keys, browser cookies, local credential files — to read window state across 69-plus providers rather than Claude alone.

ccusageCodexBarRoll your own
Form factorTerminal CLImacOS menu bar item (plus a CLI)Whatever you build
Limit percentageLocal cache onlyRead per providerLocal cache only
Covers other machinesNoYes, where exposedNo
Cost figureEstimated from a price tableEstimated, plus real billing where availableYours to maintain
Multi-providerSeveral agent CLIs69+ providersWhatever you write
Setupnpx ccusagebrew install --cask codexbarA weekend, minimum

best good workable weakest

The part worth sitting with

The instinct to build the widget is a good one, and the corrected facts make it more attractive than the folklore version, not less. The data is local, the format is simple, the parsing is a third of a second, and the headline percentage is right there in ~/.claude.json rather than needing to be reconstructed from timestamps.

But the constraint that actually determines quality was never parsing speed, and it was never even data availability. It's authority. The percentage you'd render is a copy whose freshness you don't control, of a value that lives somewhere else. A tool that authenticates and asks the provider directly is structurally better at "how close am I right now" than any amount of clever Swift over a cache file — not because your code would be worse, but because it is reading a photograph of the number rather than the number.

That reframes the decision:

The gaps on that last node are where a custom tool earns its keep, and lastModelUsage means you're starting further along than you'd expect: per-repository cost is already computed and sitting on disk. Nothing off-the-shelf charts your consumption over months, or shouts when a runaway agentic loop starts spending at triple your normal rate. Those questions are answerable entirely from local data, they don't depend on a value being fresh, and that is exactly why they're the ones worth building.

The lesson generalises past this particular ledger, in two directions. When a project looks appealing because the data is right there, check whether the number you actually care about is in that data — and if someone tells you it isn't, check that too, one directory up. A cached copy and an authoritative source look identical in a JSON file. The difference only shows up in the timestamp next to them.

References

  1. ccusage — coding agent CLI usage analysis
  2. ccusage — Blocks Reports (live monitor removal notice)
  3. CodexBar — macOS menu bar app for AI coding-provider limits