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:

typeWhat it isCosts money
assistantA model response, with message.usageYes
userYour message, or a tool resultNo
attachmentFile content attached to a turnNo
queue-operationInternal queueingNo
last-prompt, modeInternal stateNo

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 name
  • gitBranch — lets you attribute cost to a branch, and therefore to a feature
  • isSidechaintrue for subagent turns. Real spend, worth counting separately
  • requestId + message.id — the deduplication key
  • message.model — needed for pricing; a session can span models

The deduplication rule

This is the one that gets people

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 writes
  • cache_creation — the split into ephemeral_5m_input_tokens and ephemeral_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 . 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