{
  "markdown": "# cusage — Claude subscription usage explorer\n\nClaude Pro/Max subscription usage is visible only as a rough percentage. There\nis no token-level view, no per-session attribution, and no history; the API\nconsole dashboard does not cover subscription usage at all.\n\nFour local data sources hold far more than percentages, and **two of them are\nbeing garbage collected right now** — transcripts age out, and the desktop\napp's usage history is capped at a rolling 30 days. `cusage` copies all of it\ninto an archive you own, and then answers questions against that.\n\nThe archiver is the urgent half. Reporting can be rebuilt from an archive at\nany time; data that ages out is gone permanently.\n\n```\ncusage sync                    # ingest transcripts + snapshot limits (idempotent)\ncusage session --last          # one chat: full token, attribution and cost breakdown\ncusage sessions --by project   # where the tokens went\ncusage limits                  # server truth, including the limit the tray does not show\ncusage limits --history        # the meters over time, from ~2,000 archived samples\ncusage status                  # what the archive holds\ncusage attribution --by skill   # attributed requests and missing coverage\ncusage daily --since 30d        # UTC timeline, including empty days\ncusage cost --by session        # measured and estimated costs stay separate\ncusage blocks --since 7d        # observed meter cycles, local tokens alongside\ncusage cache --by model         # cache ratios and the reconciliation gap\ncusage statusline               # archived one-line view; refresh in background\ncusage doctor                  # archive, launchd, credentials and build health\ncusage export --format ndjson   # streaming archive export\n```\n\n## Install\n\nDevelopment builds require [Bun](https://bun.com) (**1.3.3 tested**).\nCommander 15 is the CLI's runtime dependency. Installed executables embed Bun\nand their dependencies; neither the checkout nor `node_modules` is needed to run.\n\n```bash\nbun install --frozen-lockfile\nmake build                  # dist/cusage and separate dist/cusage-tui\nmake install                # rebuild; atomic replacement in ~/.local/bin\ncusage sync                 # first sync: ~2s for a 240 MB corpus\n./scripts/install-agents.sh # hourly sync + 15-minute limits snapshot\n```\n\nLaunch agents execute `~/.local/bin/cusage` with `$HOME` as their working\ndirectory. Archiving a Conductor worktree no longer removes their executable\nor working directory. `make install` does not reload launch agents; rerun the\ninstaller once to migrate existing plists. The TUI binary remains a separate\nphase-5 stub, not part of the CLI import graph.\n\nEvery build embeds its Git SHA, build time, source checkout and dirty flag.\n`cusage doctor --repo /path/to/checkout` compares against that checkout's HEAD;\n`CUSAGE_REPO` overrides the embedded path. A missing checkout, different SHA,\nor dirty build is a warning, not a silently trusted build. Rebuild after edits.\n`BIN_DIR` can override the installation directory for packaging tests.\n\nThe archive lives at `~/.local/share/claude-usage/usage.db` — outside the repo,\nbecause it is data, not config. Override with `$CUSAGE_DB`. Removing the agents\n(`scripts/uninstall-agents.sh`) never touches it.\n\n## Where the data comes from\n\n| Source | What it gives | Why it is archived |\n|---|---|---|\n| `~/.claude/projects/**/*.jsonl` | one `usage` object per assistant turn, with `sessionId`, `apiBlockIndex`, `effort`, and the `attribution*` fields | ~70 days retained, then deleted |\n| `type:\"cost-state\"` records, in the same files | authoritative per-session cost, and the **only** place `claude-opus-5[1m]` is distinguishable from `claude-opus-5` | same |\n| `GET /api/oauth/usage` | the same response, current, including `weekly_scoped` | the only place `weekly_scoped` exists at all; nothing caches it |\n| `~/.claude.json` → `cachedUsageUtilization` | that response as Claude Code last cached it | single overwritten snapshot, refreshed on no schedule you can rely on |\n| `~/Library/.../Claude/plan-usage-history.json` | 15-minute longitudinal series, `{fh, sd, xu}` | rolling 30-day cap, only written while the desktop app runs |\n| Glaze `usage-history.json` | 9 sparse days, `{\"YYYY-MM-DD\": percent}` | one-time backfill |\n\nSub-agent transcripts live two levels deeper, at\n`<project>/<sessionId>/subagents/agent-*.jsonl`. A one-level glob misses about\n10% of the corpus and *all* sub-agent attribution, which is most of what this\ntool exists to show.\n\n## Deduplication\n\nClaude Code writes intermediate streaming usage snapshots and then overwrites\nthem, so the raw record count is roughly **2.1× the number of real API calls**\nand the last record for a message is frequently the smallest.\n\n- Key: `(message_id, request_id, session_id)`. When `requestId` is absent it is\n  stored as `''`, never `NULL` — see below.\n- On collision, keep max `input + output + cache_creation + cache_read`.\n- Tie-break: non-sidechain beats sidechain, then the record carrying\n  `usage.speed`.\n- Dedup is **global**, not per file. Resumed sessions replay history across\n  files.\n\nThe `requests` table is declared `WITHOUT ROWID`. This is load-bearing, not\ncosmetic: on an ordinary rowid table, `PRIMARY KEY` columns are nullable (the\nlegacy SQLite behaviour), `NULL` never compares equal to `NULL`, and the\nrecords with no `requestId` would therefore insert a fresh duplicate on\n*every* sync — while an idempotency check that re-runs the same data would\nstill pass. Verified: a rowid table accepts the same NULL-keyed row twice.\n\nWrites are a single atomic `UPSERT` implementing keep-max in SQL. A\nread-then-decide-then-write loop across 574 files degrades to \"keep last\" under\nany interruption, and \"last\" is the wrong answer.\n\n## Network\n\nThis started as *\"no automatic network calls, ever\"*, on the theory that\n`cachedUsageUtilization` was as good as the endpoint. It is not. On a full day\nof heavy use Claude Code refreshed that key **once**, and `cusage limits`\nspent 27 hours reporting `Fable 53%` while the real figure was 74%. The rule\nwas protecting a number that was wrong.\n\nSo the rule is now specific rather than absolute:\n\n- **One endpoint, `GET https://api.anthropic.com/api/oauth/usage`.** No other\n  host is contacted by shipped code. The manually invoked developer pricing\n  refresh tool downloads pricing documentation; it is not shipped or scheduled. It is a read; nothing is sent but\n  the bearer token.\n- **A 3-minute floor between attempts**, successful or not, persisted in the\n  archive so it holds across processes — a launchd agent and a statusline poll\n  are not the same process and must share one clock. `$CUSAGE_MIN_REFRESH_MS`\n  can raise that floor; nothing can lower it, including `--refresh`.\n- **`limits` and `sync` refresh when the archived copy is over 15 minutes old.**\n  `--refresh` forces a check now, `--no-refresh` or `$CUSAGE_REFRESH=off` keeps\n  a run entirely local.\n- **Every failure is soft.** No token, a 401, a timeout, no network: you get the\n  archived view and one line on stderr. Never a stack trace, never an empty\n  screen.\n- **The token is used and dropped.** Read from the login keychain\n  (`Claude Code-credentials`), falling back to `~/.claude/.credentials.json`;\n  never written to the archive, never logged, and scrubbed out of any error\n  string before it is printed.\n- **The test suite is offline.** The one test that exercises a real request\n  points at `127.0.0.1:1`.\n\n## What this tool will not do\n\n- **Never ingest content.** No `message.content` bodies, no `toolUseResult`, no\n  `history.jsonl` prompts, no `paste-cache/`, and none of the `oauthAccount` /\n  `userID` / `machineID` keys in `~/.claude.json`. Tool *names* and token\n  *counts* only. Credentials are read to sign one request and never stored.\n- **Never present a derived number as fact.** Server truth and local\n  attribution are reported as two separate things.\n- **Never present a stale number as current.** Every figure on `cusage limits`\n  carries the source it came from and how old it is, and a binding constraint\n  older than 15 minutes is labelled rather than bolded.\n\n## Known limits — read these before trusting a number\n\n- **Local data covers Claude Code only.** In 39% of sampled intervals where the\n  meter read ≥15%, there was zero Claude Code activity in the trailing five\n  hours. That is claude.ai and Desktop chat usage, and it is not on disk.\n  Correlation between the five-hour meter and local output tokens is r=0.69;\n  for the seven-day meter it is r=0.04 — **not derivable**.\n- **`cost-state` covered 166 of 513 sessions in the calibration capture.**\n  Other sessions use explicit token rates and are labelled estimated, never\n  blended with measured spend. This is API-equivalent cost, not subscription billing.\n- **Grouped cost is exact or absent.** cost-state is recorded per *session*, and\n  one session's requests can land in several projects. Summing that session\n  into each group it touched turned $90 of real spend into $226 on the test\n  fixture. Straddling sessions are counted, their cost excluded, and the column\n  marked `+`.\n- **`server_tool_use` counters read 0** even when WebSearch/WebFetch ran. Those\n  are client-side tools, not server tools. Measured across the whole corpus:\n  `web_search_requests` and `web_fetch_requests` are both zero.\n- **`ephemeral_5m + ephemeral_1h` does not reconcile with\n  `cache_creation_input_tokens`.** Corpus-wide gap: ~1K tokens out of 42M. Do\n  not assume they balance.\n- **Post-compaction `cache_read_input_tokens` drops discontinuously.** Do not\n  infer context size from it monotonically.\n- **Glaze's metric is inferred, not documented.** Its flat day→percent map\n  carries no label. It tracks `five_hour`: on every overlapping day it lands\n  within one point of that day's `fh` maximum (56/56, 24/23, 19/18, 13/12) and\n  nowhere near `seven_day`. Stored as `five_hour` with `source='glaze'` so it\n  can always be filtered out.\n\n## The limit the tray does not show\n\n`limits[]` carries three entries. The tray shows `weekly_all`. The one with\n`is_active: true` is frequently `weekly_scoped` — scoped to a single model, and\nmuch higher:\n\n```\nServer-reported limits  oauth-live · just now\n     kind           scope  pct  severity  resets\n  ▸  weekly_scoped  Fable  74%  normal    2026-09-13 05:59\n     weekly_all     all    54%  normal    2026-09-13 05:59\n     session        all     2%  normal    2026-09-10 01:29\n\n  binding constraint: weekly_scoped (Fable) at 74% · just now\n\n  meter      pct  source      age       resets\n  five_hour   2%  oauth-live  just now  2026-09-10 01:29\n  seven_day  54%  oauth-live  just now  2026-09-13 05:59\n\n  archived sources\n    source           age          five_hour  seven_day\n    oauth-live       just now            2%        54%\n    glaze            just now           15%          -  daily, inferred — not reconciled\n    desktop-history  7m ago              2%        54%\n    oauth-cache      28h 40m ago        31%        37%\n```\n\nThat bottom row is why this view looks the way it does. Reading the cache alone\ngave `37%` and `Fable 53%`; the app on screen said 54% and 74%. Both numbers\nwere archived correctly and the query picked the wrong one, so now every meter\ntakes the freshest source that carries it, keeps its provenance, and two\n*current* sources that disagree are reported side by side rather than one\nquietly winning. Nothing is averaged and nothing is interpolated.\n\n`cusage limits --history` plots the same meters over time:\n\n```\nLimit history  2026-09-06 22:18 → 2026-09-09 22:18  ·  73 × 1h  ·  210 samples\n\n  seven_day (weekly)     ▂▂▂▂▂▂▂▂▂▂▂▂·····▂▃▃▃▃▃▃▃▃▃▃▃▃▃········▃▄▄▄▄▄▄▄▄▄▄·▄▄▄▄▄▄···▅▅·▅▅▅▅▅▅▅▅▅▅  now  54%  peak 54%\n  five_hour (session)    ▂▂▂▂▂▂▂▁▂▂▂▂·····▁▂▂▁▁▁▂▂▃▃▃▅▅▂········▂▂▂▂▄▂▂▃▅▆▁·▁▁▁▁▁▁···▁▁·▁▂▂▁▂▂▂▂▁▁  now   2%  peak 65%\n  weekly_scoped (Fable)  ···········································▅···························▆▆  now  74%  peak 74%\n\n  daily peaks (UTC)\n    day         five_hour  seven_day  Fable  samples\n    2026-09-08        65%        49%    53%       64\n    2026-09-09        15%        54%    74%       66\n```\n\nBuckets hold the **peak** rather than the mean — a limit you touched at 98% and\nbacked off from is a fact about your week that an average hides. Gaps are\nrendered `·` and never bridged: those are the hours Claude.app was not running,\nand a sparkline that closes them invents a quiet period. The sparse\n`weekly_scoped` row is honest too — that series only exists for periods where\n`cusage` fetched it, because nothing on disk records it.\n\n## CLI reporting contracts\n\nRun `cusage --help` or `cusage <command> --help` for generated option help.\nUnknown options and invalid grouping values fail with exit 2. Exit 0 means\nsuccess; `session` with no match exits 1. Doctor uses 0 clear, 1 warnings,\n2 broken. `--db`, `--json`, `--csv`, `--no-color`, `--refresh` and\n`--no-refresh` work before or after the command. JSON and CSV never load the\nhuman formatter. CSV quotes commas, quotes and newlines; report metadata,\nincluding coverage, repeats on each data row.\n\nRequest reports accept `--since <duration|ISO|all>`, `--until <duration|ISO>`,\n`--project <substring>`, `--model <substring>`, and positive-integer `--limit`.\nProject/model matching is case-insensitive and literal (`%` is not a wildcard).\nWindows are start-inclusive/end-exclusive. Session lists and session exports\nselect whole sessions by their last activity, so their totals remain lifetime\ntotals; grouped session reports filter individual requests. Limit history\nretains its existing inclusive endpoint convention.\n\nEvery grouped report includes request coverage. `sessions --by ... --json`\nnow returns `{ rows, coverage, ...metadata }`, not the old bare array.\nRow limits never change coverage or cost report totals. Tool attribution counts\ncalls separately and uses each matching request once per tool; a request can\nappear under several tools, so those token columns must not be summed as\nper-tool consumption. Session details also show spawned agent identities and\nthe count of distinct per-session API block indexes.\n\n### Timeline and export\n\n`timeline --bucket day|week|month|auto` shares one query with `daily`, `weekly`,\nand `monthly` (fixed day/week/month presets). Calendar boundaries are UTC,\nweeks begin Monday, and empty buckets are emitted for each selected group.\nAuto uses the existing `chooseBucket` ladder. The default window is 30 days.\n\n`export --table requests|sessions|tools|limits --format json|ndjson|csv`\niterates SQLite rows and respects stdout backpressure. All three formats\nstream; NDJSON is the default. `--json` and `--csv` are shortcuts; conflicting\nformat choices fail. Limit samples cannot be filtered by local project/model.\n\n### Cost: exact measurements, explicitly incomplete estimates\n\n`cost [--since 30d] [--by model|project|session|day] [--measured|--estimated]`\nnever combines measured and estimated dollars into one total. The mode flags\nselect the measurement basis; they do not force measured sessions to become\nestimates. Measured cost is cumulative keep-max per session. A session spanning\nseveral groups **or partly excluded by a time/project/model filter** contributes\nrequests but no measured dollars; `+` and exclusion counts explain the gap.\nThe same `exactSessionCost` rule is used by `sessions --by` and `cost`.\nConsequently `cost --by model` can have substantial measured exclusions for\nmulti-model sessions; `cost --by session --since all` is the exact session cut.\n\n`src/pricing-snapshot.json` contains explicit per-model/per-speed input,\noutput, cache-creation and cache-read rates from the approved briefing (dated\n2026-06-24). Its initial `fetched_at` is **null**, because transcription from\nthe briefing is not an independent fetch. Fable 5.1 cache reads are $0.25/MTok;\nOpus 5 fast has its own rate. Dated aliases normalize to the base model.\nUnknown models/speeds and known `[1m]` tiers are unpriced, never guessed.\nMissing speed assumes standard; cache creation uses the briefing's 5m rate,\nnot an inferred 1h premium. Thinking tokens are already included in output.\n\nCost-state model lists back-label known `[1m]` requests for the estimator audit.\nWithout cost-state, the tier cannot be detected: estimated rows explicitly\ncount requests whose tier is unknown. Estimates are rate-based API equivalents,\nnot a measurement of subscription consumption.\n\n```bash\nbun run tools/refresh-pricing.ts\n# Review .context/pricing-source.html and prepare explicit rates, then:\nbun run tools/refresh-pricing.ts --rates /path/to/curated-snapshot.json\n```\n\nThe developer tool fetches documentation and requires reviewed structured\nrates before changing the snapshot. It never scrapes monetary values into a\nproduction price table blindly. There is no shipped `pricing` command.\n\n#### Frozen aggregate estimator acceptance\n\nCaptured from the read-only live archive on 2026-09-10 UTC (2026-09-09 local),\n`fixtures/pricing-calibration.json` holds **166 anonymous measured sessions**:\nonly model/speed token aggregates, tier model names, and cumulative dollar\ntruth. No IDs, paths, timestamps per session, or content. Regenerate explicitly\nwith `bun run tools/make-pricing-calibration.ts`; ordinary tests never read live data.\n\n| Audit measure | Frozen result |\n|---|---:|\n| Measured sessions | 166 |\n| Measured total | $752.04845015 |\n| Known-rate estimated subtotal | $500.15662315 |\n| **Scored sessions** (every request priced, positive measured cost) | **61** |\n| Their measured total | $487.72388835 |\n| Their estimated total | $413.49632860 |\n| Median absolute relative error | 18.21% |\n| p90 absolute relative error | 26.58% |\n| Worst absolute relative error | 78.36% |\n| **Declined sessions** (nothing to price, or a request refused a rate) | **105** |\n| Their measured total | $264.32456180 |\n| — of those, sessions with no ingested requests | 64 |\n\nErrors compare each session's known-rate subtotal with cumulative measured\ncost, and are computed over **scored sessions only**. That split is the point.\nA session whose every request is `[1m]` or an unknown model estimates to $0 and\nscores a relative error of exactly 1.0 — which is not a 100% estimation error,\nit is an absence graded as a wrong answer. Pooling the two buckets put 37\nsessions at exactly 1.0 and pinned p90 to 100.00%, a figure no improvement to\nthe estimator could ever have moved. The declined bucket is reported beside the\ndistribution, in dollars, so the hole stays visible.\n\nThese are still **not accuracy claims for a complete estimator**: $264.32 of\nmeasured spend has no comparable estimate at all. Within the sessions that can\nbe scored, the estimate runs about **15% low in aggregate** ($413.50 against\n$487.72), which is a real signal and not yet explained — `ephemeral_1h` cache\ncreation being priced at the 5-minute multiplier is the first suspect. The\noffline suite fixes the dollar totals and asserts median <19%, p90 <28%,\nworst <80%, without fitting any rates.\n\n### Observed meter cycles\n\n`blocks --since 7d` uses one source's five-hour meter series: desktop history\nwhen available, otherwise live OAuth, otherwise cached OAuth. A boundary is a\ndrop of at least **5 percentage points and 50%** between adjacent samples.\nThis conservative threshold can miss low-utilization resets; it does not invent\nwindows from request gaps or `api_block_index`.\n\nOAuth reset timestamps confirm observed boundaries only. They are clustered\nwithin two minutes of a fixed cluster anchor, not equality or minute truncation.\nUnconfirmed resets remain observation brackets. Gaps over 30 minutes stay `·`;\nlocal activity during gaps is not assigned to a cycle. First/last cycles are\npartial, and time-to-peak starts at the first observation. Local request/token\ncolumns are context alongside the server meter, not a causal explanation.\n\n### Operations\n\n`statusline` returns one archived line immediately, with per-meter source/age\nand UTC-today token and request counts. It carries **no dollar figure by\ndesign**: `cost --by day` correctly excludes any session straddling midnight,\nwhich is nearly always the session in progress, so a cost here would read\n$0.00 for most of the day. Tokens need no attribution or price table and are\nexact. Cost lives in `cusage cost`, where the exclusions are on screen. A\nshort-lived background CLI refreshes stale limits; it never delays the line for\nkeychain/network. The SQLite attempt claim\nis atomic across processes, and the same three-minute floor applies. Use\n`--no-refresh` or `CUSAGE_REFRESH=off` to disable the worker.\n\n`doctor` checks both launch agents and their exit status, newest request/limit\nrow ages, read-only integrity, file size, disappeared ingest sources, build\nstamp, credential presence, and transcript directory availability. Stale rows\nbeyond twice the job interval are broken checks; inactivity can also cause an\nold request row, and the diagnostic says so. Deleted source files are warnings:\nthe archive intentionally survives them. Credentials are never shown.\n\n`cache --by model|project` shows cache read/creation ratio, 5m/1h creation counts,\nand `creation - (5m + 1h)` as a signed reconciliation gap. Nothing is normalized.\n\n## Testing\n\n```bash\nbun test          # offline: frozen fixtures and synthetic edge cases\nbun run fixture   # regenerate fixtures/ from the live corpus\nbunx tsc --noEmit\n```\n\nEvery numeric assertion runs against `fixtures/`, a **frozen, scrubbed** copy of\n24 real transcripts — never against live data, which is appended to\ncontinuously and garbage collected. A baseline that drifts trains everyone to\nignore a failing test.\n\nThe fixture is safe to commit: scrubbing is an allowlist, every id, path,\nbranch and slug is remapped, all message content is dropped, and\n`tests/fixture.test.ts` re-checks for leaks on every run rather than once at\ngeneration time. `MANIFEST.json` pins the bytes; `BASELINE.json` holds the\nexpected numbers, computed by a **second, independent** dedup implementation in\n`tools/make-fixture.ts` so the tests are not just the ingester agreeing with\nitself.\n\n## Status\n\n| Phase | | |\n|---|---|---|\n| 1 | archiver — schema, ingest, limits, `sync`, both launchd agents | done |\n| 2 | `session`, `sessions`, `limits`, `limits --history`, `status` | done |\n| 3 | pricing snapshot, `cost`, frozen aggregate calibration | done |\n| 4 | attribution, timeline/aliases, export, cycles, cache, doctor, statusline, binaries | done |\n| 5 | Ink TUI | not started |\n\nThe TUI remains out of scope. `pricing --refresh` was cancelled; pricing refresh is developer-only.\n",
  "bytes": 22612,
  "sha": "ddf3a50f4668a38b6da81c24fa6884ba5b027242cca6e3ff214f0f71ac99ba7f",
  "repo_slug": "logan-lopez/claude-usage",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/okf_logan_lopez_claude_usage_knowledge_index_88e89e2e/readme"
}