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.pyReads 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:
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
Skilltool 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¶
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, forSkillblocks, 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:
- Point each developer's
settings.otel.json.patchat a shared collector instead of loopback (this becomes a real privacy/ownership decision — someone must own and secure the collector). - Stand up
otelcol-contrib+ Prometheus + Grafana via docker-compose underconfigs/monitoring/, adapting the dashboards from the source research article (8 panels: cache-hit-rate, tokens/min, skill-invocations, …). - Pin the collector binary version and gate it (the
external-cli-pinned-and-gatedpattern), since it becomes toolkit-critical infrastructure. - 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_tokenscan 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/logsevent channel). Reading latency/event data back is part of the deferred upgrade path (§6). - Deep-mode
metrics.jsonlgrows 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).