Where Claude Code stores usage data — and how to read it
Last verified
Location:
~/.claude/projects/<encoded-project-path>/<session-id>.jsonl
One directory per project — the working directory path with separators replaced by dashes — and one JSONL file per session. CLAUDE_CONFIG_DIR relocates the whole tree.
Record types
Each line is one JSON object. From a real session:
type | What it is | Costs money |
|---|---|---|
assistant | A model response, with message.usage | Yes |
user | Your message, or a tool result | No |
attachment | File content attached to a turn | No |
queue-operation | Internal queueing | No |
last-prompt, mode | Internal state | No |
Only assistant records carry usage. Everything else is context.
The assistant record
{
"type": "assistant",
"requestId": "req_...",
"uuid": "...",
"parentUuid": "...",
"timestamp": "2026-08-04T21:37:09.000Z",
"cwd": "/home/you/project",
"sessionId": "b7b07e33-...",
"gitBranch": "main",
"isSidechain": false,
"version": "...",
"message": {
"id": "msg_...",
"model": "claude-opus-5",
"role": "assistant",
"content": [ ... ],
"usage": {
"input_tokens": 2,
"output_tokens": 251,
"cache_read_input_tokens": 0,
"cache_creation_input_tokens": 36009,
"cache_creation": {
"ephemeral_5m_input_tokens": 0,
"ephemeral_1h_input_tokens": 36009
},
"service_tier": "standard"
}
}
}
Fields worth knowing:
cwd— the real project path, better than decoding the directory namegitBranch— lets you attribute cost to a branch, and therefore to a featureisSidechain—truefor subagent turns. Real spend, worth counting separatelyrequestId+message.id— the deduplication keymessage.model— needed for pricing; a session can span models
The deduplication rule
Resuming a session or retrying a request re-appends the same assistant record. Summing every line double-counts. Key on requestId + message.id and skip anything you have already seen.
Every reliable tool in this space does this. If your homemade total is higher than ccusage or /cost, this is why.
The cache fields
There are two representations, and you need to handle both:
cache_creation_input_tokens— a flat total of cache writescache_creation— the split intoephemeral_5m_input_tokensandephemeral_1h_input_tokens
The split matters because the two are priced differently: a 5-minute write is 1.25× base input, a 1-hour write is 2×. Treating all writes as one rate misprices by up to 60% of that component.
Prefer the split; fall back to the flat total treated as 5-minute, which is the cheaper rate and so never over-reports.
cache_read_input_tokens is separate again, at 0.1× base input. In practice it is the largest token count in the file by a wide margin and it needs its own rate.
A minimal reader
import json, glob, os
def entries():
root = os.environ.get("CLAUDE_CONFIG_DIR") or os.path.expanduser("~/.claude")
seen = set()
for path in glob.glob(os.path.join(root, "projects", "**", "*.jsonl"), recursive=True):
with open(path) as fh:
for line in fh:
if not line.startswith("{"):
continue
try:
r = json.loads(line)
except json.JSONDecodeError:
continue # a live session's last line can be partial
if r.get("type") != "assistant":
continue
msg = r.get("message") or {}
usage = msg.get("usage")
if not usage:
continue
key = f"{r.get('requestId','')}:{msg.get('id','')}"
if key != ":" and key in seen:
continue
seen.add(key)
yield r
Pricing the result: what Claude Code actually costs.
Or don't write one
npx @tknapp/cli agents --days 30 --by project
Handles the deduplication, the cache split, and per-model pricing from a dated, source-linked price table. No API key, no network call.
ccusage does the same job well and has more history behind it — an honest comparison.
What isn't in the logs
- Dollar figures. Only tokens. Cost is computed by you, from published rates.
- Plan quota. On Pro or Max, quota consumption isn't in these files.
- Prompt content in a convenient form. It's in
message.content, but that's a transcript, not an analytics format.
Sources
- Claude Code — Manage costs effectively
- Anthropic — Pricing (verified 2026-08-05)