CLI to track your Claude plan consumption over time. Reads the same endpoint
the app uses (https://claude.ai/api/oauth/usage), stores snapshots in DuckDB,
and displays consumption rate, projections, and charts.
git clone https://github.com/LkHideki/cmon && cd cmon
uv sync # lightweight install (without plotting libraries)
uv sync --extra plot # optional: enables the `plot` command (matplotlib/seaborn)Requires Python ≥ 3.11 and uv. Without uv,
pip install -e . also works.
cmon resolves the token in this order, stopping at the first match:
-
CLAUDE_OAUTH_TOKEN— environment variable (ideal for CI / override). -
OS secure vault — Keychain (macOS), Credential Manager (Windows), or Secret Service (Linux). Stored once, never in plain text:
cmon token set # paste the token (hidden input); or: echo $TOK | cmon token set cmon token status # where the token comes from, masked cmon token clear # remove from vault
On a headless/CI box with no secure backend,
token setrefuses to write in cleartext (keyring's plaintext fallback) — override withCMON_ALLOW_PLAINTEXT_KEYRING=1, or just useCLAUDE_OAUTH_TOKEN. -
Claude Code credential — if you're logged in, read directly from Keychain (macOS) or
~/.claude/.credentials.json(Linux/Windows). Zero friction: nothing to configure.
Auto-refresh: when the access token expires, cmon renews it automatically via
refresh_token and stores the new token in its own vault (claude-oauth-auto),
without rewriting the Claude Code credential. Renews proactively (reads
expiresAt) and reactively (if the API returns 401 — including when an old
CLAUDE_OAUTH_TOKEN is shadowing everything). Self-heals: if cmon's own refresh
chain dies (its refresh_token gets rotated/revoked after a Claude Code re-login), it
falls back to Claude Code's fresh credential and re-seeds the chain instead of getting
stuck on a 401. Side effect: the first renewal rotates Claude Code's refresh_token,
so it may ask for login once the next time it renews — after that the two tokens
become independent. token status shows validity; client_id/endpoint are configurable
via CMON_OAUTH_CLIENT_ID / CMON_OAUTH_TOKEN_URL (the latter host-allowlisted to
*.anthropic.com, so a stray .env can't redirect the refresh POST).
In short, with Claude Code logged in you need nothing — and it keeps working
even with an expired token. Without it, cmon token set stores the token securely
on any system. .env still works for step 1 (see .env.example). Run cmon --help
or cmon token --help for the rest.
cmon resolves display timezone in this order, stopping at the first match:
CMON_TZ— environment variable (quick override, doesn't persist).~/.cmon/config.json— set viacmon config set timezone <IANA name>.- UTC — default if neither is set.
cmon config set timezone America/Sao_Paulo # persist a preference
cmon config get timezone # show it
cmon config list # show all persisted preferences
cmon config clear timezone # remove it (or: cmon config clear --all)Applies to displayed timestamps (watch, trends, burn); stored data and internal
calculations stay in UTC. timezone is the only preference today; the mechanism is
built to grow (CONFIG_KEYS in cmon.py).
uv run cmon now # current usage + reset + rate/projection (--advice adds pacing tips)
uv run cmon status # one-liner for statusline/tmux/prompt
uv run cmon watch # live TUI, self-updating (Ctrl-C exits)
uv run cmon wait # block until 5h window resets, then notify
uv run cmon collect # save 1 snapshot to database (with UTC timestamp)
uv run cmon trends # consumption history: per-label summary + per-cycle peaks/anomaly
uv run cmon burn # tokens & estimated US$ (from local Claude Code logs)
uv run cmon plot # charts -> usage.png
uv run cmon install # schedule background collection in OS schedulerGlobal option --db PATH (before the subcommand) overrides CMON_DB:
uv run cmon --db ~/.cmon/usage.duckdb now.
One compact line, ideal for status bar / tmux / prompt. Exits with code 0
and prints cmon offline if the network fails (doesn't break the statusline):
5h 18% · week 42% · reset 3h18m
Blocks until the window resets and then triggers a native notification — so you
can resume the second the 5h limit clears. Or use --at N to notify when reaching N%:
uv run cmon wait # wait for 5h to reset
uv run cmon wait --window weekly_all # wait for weekly to reset
uv run cmon wait --at 80 # notify when 5h reaches 80%Prints a per-label summary first — snapshots, peak %, and total consumed % over the
window (--since 24h/7d/ISO date; --json for scripts/pipes) — then the reset-aware
cycle breakdown: it segments history at each reset and shows the peak of each cycle, the
delta versus the previous one, and a warning if the current cycle deviates from the average.
uv run cmon trends # summary + cycle breakdown
uv run cmon trends --since 24h # summary over the last 24h (accepts 7d or ISO date)
uv run cmon trends --json # JSON summary for script/pipeWhile the rest of cmon reads the official % from the endpoint, burn mines
the local Claude Code transcripts (~/.claude/projects/**/*.jsonl) to provide what
the endpoint doesn't expose: tokens and estimated US$ by model, day, project, or session
— retroactive, offline, no token required.
uv run cmon burn # by model, last 30 days (default)
uv run cmon burn --by surface # by client: terminal / vscode / app / sdk (-p)
uv run cmon burn --by project # attribution by project (where your plan went)
uv run cmon burn --since 7d # smaller window (24h, 7d, ISO date…)
uv run cmon burn --since all # full history (but Claude Code only keeps ~30d)
uv run cmon burn --jsonThe default window is 30 days — Claude Code deletes transcripts older than
that, so beyond 30d there's usually no data. Use --since all for everything
available, or --since 7d/24h/ISO date for smaller windows.
--by surface separates by where you used it (the entrypoint field in logs). Only
Claude Code, though — different accounts (by email) are not separable: transcripts
don't record the account, and claude.ai chat doesn't write logs.
Scanning is incremental (caches by mtime+size, deduplicates by uuid): the
first run reads everything (~tens of seconds on large bases), subsequent runs take
fractions of a second. The same numbers appear in watch (line burn this 5h window) and in
now --advice (model mix). Both are scoped to the current 5h rate-limit window (since the
last reset), not a rolling 5h, so they reflect only what you spent in this cycle.
burn also shows a component breakdown (input / output / cache read
/ cache write). Don't be alarmed by the total: in agentic use, cache read + write
typically account for ~80% of the cost — the model re-reading cached context with
each message, not new work. And the value is the API-equivalent cost
(pay-per-token): you pay the subscription, not this — the number shows the value
you extract from your plan (easily tens of times the monthly subscription).
Crossing both sources: the API tells you where the wall is (official % + reset), the
logs tell you how you spent it (which model/project drained it). Caveats: cost is
an estimate (price table editable at the top of cmon.py; cache write
at 2×, TTL 1h), and logs cover only Claude Code CLI — usage on claude.ai web/desktop
doesn't appear (but counts toward the official %).
The <synthetic> label (usually 0% in the model mix) is not a real model: Claude
Code stamps model: "<synthetic>" on messages it generates locally instead of calling
the API — interrupts ([Request interrupted by user]), injected notices, aborted turns.
Their usage block is all zeros, so they cost nothing and don't move your totals; they
appear only because cmon mines every transcript line carrying a usage field. Safe to
ignore — real consumption is under Opus/Sonnet/Haiku/Fable.
Self-updating dashboard: colored bars by window (green/yellow/red), rate %/h,
projection at reset, and alerts when you'll hit 100% before reset. Great to leave open
in a corner of the terminal. Each read is recorded to the database (deduped), so leaving
watch open also builds history.
uv run cmon watch # ~30s, self-tuning
uv run cmon watch -n 10 # start at ~10sIf claude.ai's Cloudflare starts returning 403 (bot-detection on the fixed polling
cadence), watch self-tunes: it backs the interval off on each error (up to 5min), adds
jitter, eases back once reads succeed again, and remembers the safe interval for the next
run — so you can leave it open without tripping the block.
collect --alert fires two kinds of alert to stderr (cron emails them) plus a best-effort
native notification (macOS/Linux):
- rate-based — at the current pace a window will hit 100% before reset (also shown by
cmon now); - time-based — a window is within its lead of resetting: the 5h window in ≤
CMON_ALERT_LEADminutes (default 60) and the weekly window in ≤CMON_ALERT_LEAD_WEEKLYminutes (default 180). Each fires once per cycle (deduped on its own reset), as a desktop notification.
Set CMON_HOOK to run your own command on the time-based alert: cmon executes it via the
shell with the message in $CMON_ALERT_MSG (best-effort — a broken hook never breaks
collection). Pipe it into a bell, a webhook, tmux display-message, etc.
*/20 * * * * cd ~/cmon && /path/to/uv run cmon collect --alertexport CMON_ALERT_LEAD=30 # alert 30min before the 5h reset
export CMON_ALERT_LEAD_WEEKLY=180 # alert 3h before the weekly reset
export CMON_HOOK='tmux display-message "cmon: $CMON_ALERT_MSG"' # run on that alertcmon now answers immediately "how long until my 5h window resets" and,
if there's history, projects whether you'll hit the limit before then:
Current usage:
Current session █·················· 7% resets in 4h 25min
All models ████████·········· 41% resets in 3d 22h
Fable only ████████·········· 43% resets in 3d 22h ←active
5h window: 7% used — expires in 4h 25min.
Rate: 2.1%/h → projection at reset: 16%.
Add --advice for the full pacing view — the goal being to spend close to 100% of the
weekly limit by reset, without exhausting early and without hitting the 5h window (which
locks you out). For each window it shows the observed rate, the target %/h to zero slack, and
the projection at reset:
- projection < 100% → upside: quota left, you can intensify or use a stronger model;
- projection > 100% → shortfall: how many hours until you hit 100% before reset and how much you need to throttle.
The rate cuts at the last reset (so it adapts to 5h, 7d, or 72h windows — Anthropic resets the
"weekly" at a fixed time, not always exactly 7 days) and is exponentially weighted: recent
snapshots count more (half-life ~1h for the 5h window, ~12h weekly), so the projection tracks your
current pace rather than a flat cycle average. Finally, --advice sends the
numbers to Claude Sonnet (claude -p, cheap) for 3 actionable tips; use --no-ai for local
projections only, no quota cost.
plot/trends/alerts become useful with history. Every command that queries the
API — now, watch, wait — already records its reading (deduped within
CMON_DEDUP_SECS), so history accrues from normal use, not only from scheduled collect.
Persisting is best-effort: if the single-writer DB is busy (e.g. watch is open) the read
still succeeds, it just doesn't save that one point. For unattended, regular sampling, leave
collect scheduled in the OS native scheduler:
uv run cmon install # every 20min (launchd/systemd/schtasks)
uv run cmon install -i 10 # every 10min
uv run cmon install --dry-run # only show what it would do
uv run cmon uninstall # removeIn the background the token comes from the OS vault or Claude Code credential — your shell's
CLAUDE_OAUTH_TOKEN env var is not inherited, so run cmon token set
if that's your case. Prefer manual cron? Still works:
*/20 * * * * cd ~/cmon && /path/to/uv run cmon collect --alert- Source:
limits[]array from the endpoint —session(5h window),weekly_all(all models), andweekly_scoped(per-model, e.g. Fable). - Consumption: difference in
percentbetween snapshots; drops = window reset (discarded), not consumption. - Requires the
User-Agent: claude-cli/...header, otherwise claude.ai's Cloudflare responds with 403. - Resilience:
fetchretries on 429/5xx/network with backoff (respects a clampedRetry-After); 401/403 fail with a clear message, butwatchturns a 403 into an adaptive backoff (see above) rather than giving up, and token resolution self-heals a dead refresh chain from the Claude Code credential.collectdeduplicates very close readings (CMON_DEDUP_SECS, default 60s;--forceignores) and exits with code ≠ 0 on failure, so cron logs the error instead of silencing it.
Everything lives in a single file, cmon.py — commands are functions now,
collect, watch, etc., wired to argparse in main(). Easy to read top to bottom.
uv sync --extra plot # install everything, including plotting libs
uv run ruff check . # lint (config in pyproject.toml)
uv run ruff check --fix . # auto-fix what it can
uv run cmon <cmd> # run straight from sourceCI (.github/workflows/ci.yml) runs ruff + CLI smoke tests
on Python 3.11–3.13. PRs welcome: keep ruff green and the file style lean. Useful
environment variables: CMON_DB (database path), CMON_RETRIES, CMON_DEDUP_SECS,
CMON_ALERT_LEAD / CMON_ALERT_LEAD_WEEKLY (minutes before the 5h / weekly reset to alert),
CMON_HOOK (command run on that alert),
CMON_OAUTH_CLIENT_ID / CMON_OAUTH_TOKEN_URL (OAuth refresh overrides; URL allowlisted to
*.anthropic.com), CMON_ALLOW_PLAINTEXT_KEYRING (permit saving to a cleartext keyring backend),
CMON_TZ (display timezone override; see Config).
Uses a private, undocumented Anthropic endpoint; may change without notice. Only accesses your own account. License MIT.