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.

  • nameusage-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

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.

~/.claude/skills/usage-cost-tracking/SKILL.md

--- name: usage-cost-tracking description: Measures and reduces Claude Code token usage and spend using /usage (alias /cost and /stats), /context, the status line cost fields, local… license: MIT ---

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

SourceShowsWho
/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, loopsEveryone
/contextWhat fills the current context window: system prompt, tools, MCP, memory files, messagesEveryone
Status line cost.total_cost_usd, context_window.used_percentage, rate_limits.*Live, always visibleSee statusline-scripts
claude -p --output-format jsontotal_cost_usd, num_turns, usage per scripted runCI and scripts
~/.claude/projects/*/*.jsonl transcriptsPer-response usage for every local sessionscripts/usage_report.py
/insightsHTML report on working patterns and friction, not tokensEveryone
Console usage page, Claude Code Analytics APIPer-user spend for API organizationsAdmins
Team/Enterprise analytics and spend report CSVPer-user spend and adoptionAdmins
OpenTelemetry (CLAUDE_CODE_ENABLE_TELEMETRY=1)Token and cost metrics streamed to your stack, any providerPlatform 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

EvidenceDriverFix
One session with a huge token total; cache_read dominatesLong-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 missesBreaks over the cache TTL, changing tools/MCP mid-session, switching modelsBatch work; avoid toggling MCP servers or models mid-task; check the “likely cause” on the cache line
Opus dominates the model breakdown for routine editsModel mismatchDefault to Sonnet for implementation; /model opus only for hard design or debugging
High output tokens per requestEffort 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 modelSet model: haiku or sonnet in subagent definitions; fewer parallel agents
/context shows large MCP or memory blocksAlways-loaded contextDisable 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 transcriptsRaw output flooding contextFilter in a hook (only failures), pipe through tail/grep, delegate noisy work to a subagent
/usage Loops rows or scheduled tasksRecurring runs sending full context while idleLower frequency, stop finished loops, run them in fresh short sessions
Agent teams activeEach teammate has its own contextSmall teams, Sonnet teammates, shut them down when done
Repeated /compact on large contextsCompaction 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

  1. Resume it (claude -r <name>) and run /context to see the composition.
  2. In the transcript, find where tokens jumped: large tool_result blocks, file reads of whole generated files, repeated test runs with full output.
  3. 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 modelPricing in 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 168 lines · 7.1 KB
#!/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.

esc