Usage and Cost Tracking skill for Claude Code
Find where the tokens went, then cut the top three. Claude measures before advising: a transcript report breaks seven days of usage down by day, model, project, session and skill with cache hit ratio and subagent share, then maps the evidence to concrete drivers such as all-day sessions, Opus defaults, inherited subagent models or bloated MCP context. Fixes come with guardrails like budget caps for scripts and a re-measurement on a comparable week.
- name
usage-cost-tracking - size141 lines · ≈1,950 tokens
- files2
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Measures and reduces Claude Code token usage and spend using /usage (alias /cost and /stats), /context, the status line cost fields, local transcript analysis, OpenTelemetry metrics, and Console or Team analytics, then cuts waste through model and effort choice, clearing and compaction habits, MCP and CLAUDE.md trimming, subagent model selection, and --max-budget-usd caps for scripts. Includes a transcript usage report script. Use when the user asks why Claude Code is expensive, hits usage limits, wants a spend report, or needs budgets for a team or CI. Try asking
- Why is my Claude Code usage so high this week? Show me where it goes
- I keep hitting my 5-hour limit by lunch — what should I change?
- Put a cost cap on our claude -p CI jobs and report spend per run
Use it when
- Spend or plan usage is higher than expected
- You hit session or weekly limits regularly
- A team needs per-user reporting, budgets, or contracted-rate estimates
- Scripts or CI runs need hard budget and turn caps
Skip it when
- You only want a live readout of context and cost — use statusline-scripts instead
- The issue is a single session's context getting cluttered — use context-window-hygiene instead
How to check the result
The before and after usage reports cover comparable workloads, the named drivers shrink in their own rows, and capped CI runs log total_cost_usd under the budget.
The common mistake it prevents
Summing raw transcript lines without de-duplicating by message id, which counts each API response several times and inflates every figure.
Cost work is done when you can say where the tokens went (by session, model, project, and category), which two or three habits or settings account for most of it, what was changed, and the before/after number on a comparable workload. Advice without a measurement is a guess.
Where the numbers come from
| Source | Shows | Who |
|---|---|---|
/usage (aliases /cost, /stats) | Session cost estimate, tokens by model, prompt cache line; on Pro/Max/Team/Enterprise also plan limit bars and attribution to skills, subagents, plugins, MCP servers, loops | Everyone |
/context | What fills the current context window: system prompt, tools, MCP, memory files, messages | Everyone |
Status line cost.total_cost_usd, context_window.used_percentage, rate_limits.* | Live, always visible | See statusline-scripts |
claude -p --output-format json | total_cost_usd, num_turns, usage per scripted run | CI and scripts |
~/.claude/projects/*/*.jsonl transcripts | Per-response usage for every local session | scripts/usage_report.py |
/insights | HTML report on working patterns and friction, not tokens | Everyone |
| Console usage page, Claude Code Analytics API | Per-user spend for API organizations | Admins |
| Team/Enterprise analytics and spend report CSV | Per-user spend and adoption | Admins |
OpenTelemetry (CLAUDE_CODE_ENABLE_TELEMETRY=1) | Token and cost metrics streamed to your stack, any provider | Platform teams |
All in-product dollar figures are client-side estimates at list price (or at
modelPricing rates set in managed settings). The authoritative bill is in the Console
or cloud provider billing. Subscribers should watch plan limit bars, not dollars.
Workflow
- [ ] 1. Establish the baseline: run the transcript report for 7 days
- [ ] 2. Find the top cost drivers (table below) with evidence
- [ ] 3. Inspect one expensive session in detail (/context, transcript)
- [ ] 4. Apply the 2-3 highest-impact fixes
- [ ] 5. Set guardrails: status line, budgets for scripts, team limits
- [ ] 6. Re-measure after a comparable week; report before/after
1. Baseline
python3 scripts/usage_report.py --days 7 --top 10
python3 scripts/usage_report.py --days 7 --project my-repo --prices prices.json # optional $
python3 scripts/usage_report.py --days 30 --json > usage-30d.json
The script reads local transcripts, de-duplicates responses (each API response is logged
as several lines sharing one message id), and prints tokens by day, model, project, top
sessions, and attributed skill, plus cache hit ratio and subagent share. Dollar figures
appear only with a --prices file you fill from the current pricing page; the script
never guesses rates. It covers this machine only, not claude.ai or other devices.
2. Cost drivers and fixes
| Evidence | Driver | Fix |
|---|---|---|
| One session with a huge token total; cache_read dominates | Long-lived session re-sending a large history every turn | /clear between unrelated tasks; /rename then /resume to return; start fresh sessions per task |
Low cache hit ratio (< 80%) or /usage cache misses | Breaks over the cache TTL, changing tools/MCP mid-session, switching models | Batch work; avoid toggling MCP servers or models mid-task; check the “likely cause” on the cache line |
| Opus dominates the model breakdown for routine edits | Model mismatch | Default to Sonnet for implementation; /model opus only for hard design or debugging |
| High output tokens per request | Effort too high for the task; verbose style | /effort medium or low for mechanical work; Concise output style |
| Subagent share > 50% | Fan-out with inherited expensive model | Set model: haiku or sonnet in subagent definitions; fewer parallel agents |
/context shows large MCP or memory blocks | Always-loaded context | Disable unused MCP servers in /mcp; prefer CLIs (gh, aws); move task-specific CLAUDE.md sections into skills; keep CLAUDE.md under 200 lines |
| Big tool outputs (logs, test runs) in transcripts | Raw output flooding context | Filter in a hook (only failures), pipe through tail/grep, delegate noisy work to a subagent |
/usage Loops rows or scheduled tasks | Recurring runs sending full context while idle | Lower frequency, stop finished loops, run them in fresh short sessions |
| Agent teams active | Each teammate has its own context | Small teams, Sonnet teammates, shut them down when done |
Repeated /compact on large contexts | Compaction itself reads the whole history | /clear when continuity is not needed; compact earlier with a focus instruction |
Thinking tokens bill as output. On models with a fixed thinking budget,
MAX_THINKING_TOKENS lowers it; adaptive models use effort levels instead.
3. Inspect one expensive session
- Resume it (
claude -r <name>) and run/contextto see the composition. - In the transcript, find where tokens jumped: large
tool_resultblocks, file reads of whole generated files, repeated test runs with full output. - Note what Claude explored that CLAUDE.md could have answered (missing commands, map).
4. Guardrails
- Status line with context % and cost for every developer (
statusline-scripts). - Scripts and CI: always cap runs.
claude -p "$PROMPT" --output-format json --max-turns 15 --max-budget-usd 2.00 \
--model sonnet --allowedTools "Read,Grep,Glob,Bash(npm test *)" \
| jq '{cost: .total_cost_usd, turns: .num_turns, ok: (.is_error | not)}'
--max-budget-usd checks the client-side estimate and includes subagents; spend can pass
the cap slightly, so leave headroom. Log total_cost_usd per job to spot drift.
- Teams on the Console: workspace spend limits and a workspace rate limit on the auto-created Claude Code workspace. Team/Enterprise: spend limits per org, group, or member in admin settings. Cloud providers: budgets in the cloud console, OpenTelemetry or a gateway for per-user attribution.
- Contracted rates: set
modelPricingin managed settings so estimates match invoices.
OpenTelemetry quick start (send to your collector):
export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_METRICS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
export OTEL_EXPORTER_OTLP_ENDPOINT=http://collector.internal:4317
Check the monitoring docs for current metric names before building dashboards.
5. Re-measure
Compare the same weekday span or the same task type. Report tokens per active day or per merged PR rather than raw totals, which move with workload.
Gotchas
- Treating subscriber “cost” figures as a bill. On Pro/Max the session dollar figure is informational; limits are what matter.
- Switching models to escape a session or weekly limit; those limits are shared across models (model-specific limit messages are the exception).
- Restarting sessions constantly to “save tokens” loses cache benefits; clear between tasks, not between every message.
- Comparing totals across weeks with different workloads. Normalize.
- Reading transcripts without de-duplicating by message id overcounts several-fold.
- Background usage exists (summaries, prompt suggestions) but is small; it is rarely the driver.
Output
**Window:** 7 days, this machine · **Total:** 412M tokens (cache read 93%), est. $164 at list
**Top drivers:**
1. Two all-day sessions = 58% of tokens (history re-sent each turn) -> clear per task
2. Opus default for routine edits = 71% of spend -> Sonnet default, Opus on demand
3. Explore subagents inherit Opus, 34% share -> `model: haiku` in agent definitions
**Changes made:** model default, subagent models, MCP: disabled 3 unused servers (-9k ctx tokens)
**Guardrails:** status line with ctx/cost; CI runs capped at --max-budget-usd 2
**Re-measure:** 2026-10-27, same report command Bundled files
Loaded only when a step in the skill needs them. Review them here before installing.
scripts/usage_report.py
#!/usr/bin/env python3
"""Summarize Claude Code token usage from local session transcripts.
Reads ~/.claude/projects/*/*.jsonl (or a path you pass), de-duplicates API responses
(one response is logged as several lines that share message.id), and reports tokens by
day, project, model, and session, plus cache hit ratio, subagent share, and skill
attribution where the transcript records it.
Usage:
python3 usage_report.py [--root ~/.claude/projects] [--days 7] [--project substr]
[--top 10] [--prices prices.json] [--json]
prices.json (optional, USD per million tokens; you supply current rates):
{"claude-sonnet-5": {"input": 3, "output": 15, "cache_write": 3.75, "cache_read": 0.3}}
Model keys match by prefix, so "claude-sonnet" covers every Sonnet id.
Without --prices the report shows tokens only (no dollar figures are guessed).
Exit 0 on success, 1 if no usage records were found.
"""
import argparse
import json
import sys
from collections import defaultdict
from datetime import datetime, timedelta, timezone
from pathlib import Path
FIELDS = ("input", "output", "cache_write", "cache_read")
def iter_records(root: Path, since: datetime, project_filter: str):
files = [root] if root.is_file() else sorted(root.glob("*/*.jsonl")) + sorted(root.glob("*/*/subagents/*.jsonl"))
seen = set()
for f in files:
project = f.parent.name if root.is_dir() else f.parent.name
if f.parent.name == "subagents":
project = f.parent.parent.parent.name
if project_filter and project_filter not in project:
continue
try:
fh = open(f, encoding="utf-8", errors="replace")
except OSError:
continue
with fh:
for line in fh:
if '"usage"' not in line:
continue
try:
d = json.loads(line)
except json.JSONDecodeError:
continue
msg = d.get("message") or {}
usage = msg.get("usage")
if d.get("type") != "assistant" or not isinstance(usage, dict):
continue
key = msg.get("id") or d.get("requestId") or d.get("uuid")
if key in seen:
continue
seen.add(key)
ts = d.get("timestamp", "")
try:
when = datetime.fromisoformat(ts.replace("Z", "+00:00"))
except ValueError:
continue
if when < since:
continue
yield {
"day": when.date().isoformat(),
"project": project,
"session": d.get("sessionId", f.stem),
"model": msg.get("model", "unknown"),
"sidechain": bool(d.get("isSidechain")) or f.parent.name == "subagents",
"skill": d.get("attributionSkill"),
"input": usage.get("input_tokens", 0) or 0,
"output": usage.get("output_tokens", 0) or 0,
"cache_write": usage.get("cache_creation_input_tokens", 0) or 0,
"cache_read": usage.get("cache_read_input_tokens", 0) or 0,
}
def cost(rec, prices):
if not prices:
return None
for prefix, rate in sorted(prices.items(), key=lambda kv: -len(kv[0])):
if rec["model"].startswith(prefix):
return sum(rec[k] * rate.get(k, 0) for k in FIELDS) / 1_000_000
return None
def fmt(n):
for unit, size in (("B", 1e9), ("M", 1e6), ("k", 1e3)):
if n >= size:
return f"{n / size:.1f}{unit}"
return str(int(n))
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--root", type=Path, default=Path.home() / ".claude" / "projects")
ap.add_argument("--days", type=int, default=7)
ap.add_argument("--project", default="")
ap.add_argument("--top", type=int, default=10)
ap.add_argument("--prices", type=Path)
ap.add_argument("--json", action="store_true")
a = ap.parse_args()
prices = json.loads(a.prices.read_text()) if a.prices else None
since = datetime.now(timezone.utc) - timedelta(days=a.days)
groups = {k: defaultdict(lambda: defaultdict(float)) for k in ("day", "project", "model", "session", "skill")}
total = defaultdict(float)
count = 0
for r in iter_records(a.root.expanduser(), since, a.project):
count += 1
c = cost(r, prices)
r["tokens"] = sum(r[k] for k in FIELDS)
for g in groups:
key = r[g] if g != "skill" else (r["skill"] or "(none)")
if g == "session":
key = f"{r['project'][-30:]} {r['session'][:8]}"
for k in FIELDS + ("tokens",):
groups[g][key][k] += r[k]
groups[g][key]["requests"] += 1
if c is not None:
groups[g][key]["usd"] += c
for k in FIELDS + ("tokens",):
total[k] += r[k]
if r["sidechain"]:
total["sidechain_tokens"] += r[k] if k == "tokens" else 0
total["requests"] += 1
if c is not None:
total["usd"] += c
if not count:
print(f"no usage records in the last {a.days} day(s) under {a.root}", file=sys.stderr)
return 1
all_input = total["input"] + total["cache_write"] + total["cache_read"]
summary = {
"days": a.days,
"requests": int(total["requests"]),
"tokens": {k: int(total[k]) for k in FIELDS},
"cache_hit_ratio": round(total["cache_read"] / all_input, 3) if all_input else None,
"subagent_share": round(total["sidechain_tokens"] / total["tokens"], 3) if total["tokens"] else 0,
"usd_estimate": round(total["usd"], 2) if prices else None,
"groups": {g: {k: dict(v) for k, v in d.items()} for g, d in groups.items()},
}
if a.json:
print(json.dumps(summary, indent=2))
return 0
print(f"Last {a.days} day(s): {summary['requests']} API responses")
print("tokens " + " ".join(f"{k}={fmt(total[k])}" for k in FIELDS))
print(f"cache hit ratio {summary['cache_hit_ratio']:.0%} | subagent share {summary['subagent_share']:.0%}"
+ (f" | est. ${summary['usd_estimate']:.2f} (your price file, list-price estimate)" if prices else ""))
for g, title in (("day", "By day"), ("model", "By model"), ("project", "By project"),
("session", f"Top {a.top} sessions"), ("skill", "By attributed skill")):
rows = sorted(groups[g].items(), key=(lambda kv: kv[0]) if g == "day" else (lambda kv: -kv[1]["tokens"]))
if g in ("session", "project", "skill"):
rows = rows[: a.top]
if g == "skill" and len(rows) == 1 and rows[0][0] == "(none)":
continue
print(f"\n{title}")
for key, v in rows:
usd = f" ${v['usd']:.2f}" if prices else ""
print(f" {key[:44]:44} {fmt(v['tokens']):>8} tok out {fmt(v['output']):>7} "
f"req {int(v['requests']):>5}{usd}")
return 0
if __name__ == "__main__":
sys.exit(main())
About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.