Skip to content

Monitoring Claude Code usage

A zero-infrastructure way to see how Claude Code is performing for you — cache efficiency, token totals, and which tools and skills you actually invoke — using one command and no setup.

TL;DR   python3 configs/monitoring/cc-metrics.py

Reads your own local Claude Code session logs. No telemetry to enable, no server to run, no dependencies to install. Prints numbers only — never the contents of your prompts, responses, or tool calls.


1. What this is — and what it isn't

Is: a local, on-demand readout. cc-metrics.py parses the session transcripts Claude Code already writes to ~/.claude/projects/ and prints an aggregate. You run it when you want a number; nothing runs in the background.

Isn't: a hosted dashboard, a team-aggregate view, or a live monitor. Those belong to the deferred full stack (Prometheus/Grafana) described in §6 — built only if a real team need appears. This page is the minimal, prove-demand-first version (SPEC-129).

Two data sources exist and this tool defaults to the one that needs no setup:

Local JSONL (default) OTel deep-mode (opt-in, §5)
Enable nothing — always written apply settings.otel.json.patch
Read after the fact? yes, full history only while a receiver is live
Token accuracy cache fields exact; input_tokens approximate authoritative
Breakdowns per-tool + per-skill counts per-skill counts only (no per-tool)

2. Quick start

From the toolkit root:

python3 configs/monitoring/cc-metrics.py

Sample output:

Claude Code usage readout
========================================
sessions       231
lines read     49454
lines skipped  0

cache-read ratio   94.21%   (key signal)
tokens (approx.):
  input             31,935,181
  output            14,782,699
  cache read      2,586,441,450
  cache creation      127,133,218

tool invocations:
  Bash            3,052
  Read            1,999
  Edit              847
  Write             394
  Skill              73
  ...

skill invocations:
  vt-c-2-plan         12
  vt-c-activate        9
  ...

Reading each line:

  • sessions — session files that contributed at least one assistant record.
  • lines read / skipped — JSONL lines parsed vs. skipped (malformed or streaming-partial lines are skipped, never fatal — see §7).
  • cache-read ratio — the headline signal (§3).
  • tokens (approx.) — summed token buckets. Approximate on purpose (§3).
  • tool / skill invocations — how many times each tool ran, and the per-skill breakdown. The Skill tool total reconciles with the skill list.

Useful flags:

python3 configs/monitoring/cc-metrics.py --since 7d        # last 7 days only (7d/24h/30m)
python3 configs/monitoring/cc-metrics.py --project <slug>  # one project directory
python3 configs/monitoring/cc-metrics.py --json            # machine-readable

--json shape:

{ "sessions": N, "lines_read": N, "lines_skipped": N,
  "tokens": { "input": N, "output": N, "cache_read": N, "cache_creation": N, "approx": true },
  "cache_read_ratio": 0.94,
  "tools": { "<name>": count },
  "skills": { "<name>": count } }

3. Cache-read ratio — the key signal

cache_read_ratio = cache_read / (input + cache_read + cache_creation)

The denominator includes all three token buckets, so the ratio is the share of your input that was served from the prompt cache rather than re-sent. It is the single number worth watching because it tracks how well your context (CLAUDE.md, system prompt, tool definitions) is being reused across turns. The same task run under two configurations can differ dramatically — a well-cached setup sits high (90%+ is common for steady Claude Code work); a low ratio means cache is being invalidated and you are paying full input cost every turn.

This ratio is computed from the cache token buckets, which are recorded accurately in the JSONL. Treat it as reliable.

Token totals are labelled "(approx.)" and you should read them that way. input_tokens in the local JSONL is sometimes a streaming placeholder that undercounts; the cache buckets are accurate but the absolute input total may not be. Use the totals for order-of-magnitude and trend, not billing. For authoritative totals, use OTel deep-mode (§5).


4. Privacy — exactly what is read and exported

The session JSONL is a full transcript: it contains your prompts, the assistant's responses, and every tool input and output. The readout never emits any of that. cc-metrics.py extracts only:

  • numeric fields under message.usage (the token buckets), and
  • tool names (message.content[].name) and, for Skill blocks, the skill name (input.skill).

It never reads or prints prompt text, response text, tool inputs, tool outputs, or file contents. This is enforced by a privacy test that seeds a fixture with a secret-looking string and asserts it can never appear in the output (plugins/core-standards/tests/test-cc-metrics.sh, test_privacy).

Deep-mode (§5) exports more, so read this before enabling it. With the OTel patch, Claude Code exports metric names + values plus a set of attributes. It does not export prompt/response/tool content — those need the OTEL_LOG_* content flags, which the patch deliberately leaves unset (OTEL_LOG_USER_PROMPTS, OTEL_LOG_ASSISTANT_RESPONSES, OTEL_LOG_TOOL_DETAILS, OTEL_LOG_TOOL_CONTENT, OTEL_LOG_RAW_API_BODIES). However, the metadata payload still carries identifying attributes: user.email, user.account_uuid/account_id, organization.id, session.id, terminal.type. Because the endpoint is loopback, this stays on your machine — and otel-receiver.py strips those identifying attributes before writing metrics.jsonl. The patch also sets OTEL_METRICS_INCLUDE_SESSION_ID=false and OTEL_METRICS_INCLUDE_ACCOUNT_UUID=false to drop two of them at the source.


5. Optional OTel deep-mode

When you want authoritative token totals (not the approximate JSONL ones), enable Claude Code's built-in OpenTelemetry export into a tiny local receiver. This is opt-in and default-off. What it surfaces today is authoritative token totals plus per-skill counts — the minimal receiver persists only /v1/metrics and drops the /v1/logs event channel, so latencies and per-event data are not read back (that is part of the deferred upgrade path, §6).

# 1. start the loopback receiver (stdlib, binds 127.0.0.1:4318)
python3 configs/monitoring/otel-receiver.py --out metrics.jsonl

# 2. opt in: apply the 9 env vars from
#    configs/monitoring/settings.otel.json.patch to your ~/.claude/settings.json
#    (see settings.otel.json.patch.README.md), then restart Claude Code.

# 3. after a session, read the authoritative totals back
python3 configs/monitoring/cc-metrics.py --otel metrics.jsonl

The receiver requires OTEL_EXPORTER_OTLP_PROTOCOL=http/json (set by the patch) so the payload is plain JSON — no protobuf/grpc dependency. It binds loopback only and refuses any non-loopback host. Opt out at any time by removing the env block from ~/.claude/settings.json (or setting CLAUDE_CODE_ENABLE_TELEMETRY=0) and restarting; export stops immediately and nothing was persisted off-machine.

When it's worth it: you need billing-grade token totals or are validating a caching change and want exact numbers. For a quick "how's my cache doing", the default JSONL readout (§2) is enough.


6. Upgrade path (deferred team stack)

The original SPEC-129 envisioned a full team-aggregate stack: otel-collector → Prometheus → Grafana with dashboards and a cache-hit-rate alert. That is not built — at ~4 developers, aggregate metrics are high-variance and the maintenance tax exceeds the insight. It is preserved here as the demand-gated upgrade path.

Graduate when a concrete team-aggregate need appears (e.g. a decision that requires cross-developer cache/cost comparison, or an adoption metric that needs objective team data). At that point:

  1. Point each developer's settings.otel.json.patch at a shared collector instead of loopback (this becomes a real privacy/ownership decision — someone must own and secure the collector).
  2. Stand up otelcol-contrib + Prometheus + Grafana via docker-compose under configs/monitoring/, adapting the dashboards from the source research article (8 panels: cache-hit-rate, tokens/min, skill-invocations, …).
  3. Pin the collector binary version and gate it (the external-cli-pinned-and-gated pattern), since it becomes toolkit-critical infrastructure.
  4. Add the Prometheus Alertmanager rule for cache-hit-rate < 50% over 1h (console/log notification by default).

Until that trigger fires, this section is the spec for the full stack.


7. Limitations

  • Small-team variance. With ~4 developers, cross-developer aggregates are low-signal; the per-user readout is the useful unit at this scale.
  • Token totals are approximate. input_tokens can be a streaming undercount; the cache-read ratio (from accurate cache buckets) is the reliable number. Use deep-mode (§5) for authoritative totals.
  • JSONL schema is undocumented. The record shape is reverse-engineered from real files (verified 2026-07-04 against 374 sessions; see the spec's research.md). A Claude Code update could rename fields. The reader fails soft — malformed or changed lines are skipped and counted (lines skipped), never fatal — so schema drift degrades gracefully rather than crashing, and a rising skip count is the signal to re-run the field audit.
  • No latencies or per-event data in either mode. The default reads token/tool/skill counts from JSONL; deep-mode's receiver persists only /v1/metrics (it drops the /v1/logs event channel). Reading latency/event data back is part of the deferred upgrade path (§6).
  • Deep-mode metrics.jsonl grows unbounded. The receiver appends and never rotates. For an occasional readout this is fine, but if you leave the receiver running long-term, prune or rotate the file periodically (e.g. delete it between measurement windows — the reader has no state to lose).

Source: SPEC-129. Code under configs/monitoring/ (see configs/monitoring/README.md in the repository).