A local, zero-dependency dashboard that paces your weekly Claude Code usage
so you don't burn the allowance by Wednesday. It reads the transcripts Claude
Code already writes to ~/.claude/projects/, computes what share of your weekly
allowance you've spent, and tells you how much today gets.
Nothing leaves the machine. No API keys, no credentials, no network calls — the
server binds to localhost and only reads files you already have.
Not affiliated with, endorsed by, or supported by Anthropic. "Claude" and "Claude Code" are trademarks of Anthropic. This tool estimates consumption from local transcript data; it is not an official usage meter, and the numbers are an approximation (see Known limits).
macOS and Windows both have always-on installers — a LaunchAgent on macOS, a built launcher exe plus a Startup-folder shortcut on Windows. The server and dashboard are plain Node and should run anywhere Claude Code does.
- scan — incremental reader for
~/.claude/projects/**/*.jsonl - weighting — relative consumption model incl. cache write/read multipliers
- budget — weekly window, limit estimation, buffer+debt allocation
- server — localhost HTTP + JSON API, zero dependencies
- web — dashboard with drag/scroll availability calendar
- percentages — all display in % of weekly allowance (no API dollars)
- calibration — enter the %
/usageshows, tool solves for the real limit - 5-hour block — start inferred from transcript activity
- launchagent — always-on install script (macOS)
- windows launcher — built exe + Start Menu/Desktop/Startup shortcuts
- durable plan — atomic writes + serialized saves; settings survive concurrency
- cross-file dedup — warm and cold scans agree (was inflating toward 2x)
- setup gate — blocking dialog collects the reset window + both
/usagereadings first - live readings — status-line hook forwards the real 5h / weekly % and reset times
- advisor usage — advisor calls are counted, priced at the advisor's own model
- Full Disk Access — grant it to
nodeso the LaunchAgent can read ~/Desktop - limit pinning — auto-pins on first real rate-limit hit; unverified until one occurs
Requires Node 20+. No npm install — there are no dependencies.
git clone https://github.com/andoodle/yet-another-usage-tracker.git
cd yet-another-usage-tracker
node src/server.mjsThen open http://localhost:4478. Set BUDGET_PORT to use a different port.
Always-on (recommended, macOS) — runs at login, restarts if it dies:
scripts/install-launchagent.shThen just open http://localhost:4478.
Desktop shortcut (no Terminal window; opens the dashboard, and starts the server first if it isn't answering):
scripts/make-desktop-shortcut.sh # -> ~/Desktop
scripts/make-desktop-shortcut.sh /Applications
scripts/make-desktop-shortcut.sh --uninstallAlways-on (recommended, Windows) — builds a small launcher exe, then adds Start Menu, Desktop, and Startup-folder shortcuts:
.\scripts\install-windows.ps1
.\scripts\install-windows.ps1 -NoAutostart # shortcuts only, no login start
.\scripts\install-windows.ps1 -UninstallNothing is installed to build it. csc.exe (the C# compiler) ships inside
Windows as part of the .NET Framework, and the icon is rendered by this repo's
own scripts/make-icon.mjs. Node stays the only prerequisite.
The exe is a ~10KB launcher, not a self-contained binary: it checks whether
the port is already answering, spawns node src/server.mjs hidden if not
(logging to claude-budget.log), waits for the port, then opens your browser.
--serve skips the browser, which is what the Startup shortcut uses so login
doesn't hand you a tab you didn't ask for. Launching it twice won't start a
second server.
A Node SEA build would produce a true standalone exe, but at ~110MB, plus a bundler dependency and a rewrite of the static-file serving to read from SEA assets. Node is already required to run the server, so the thin launcher buys the same double-clickability for 0.01% of the size and no source changes.
Autostart is a Startup-folder shortcut rather than a Scheduled Task or a service, deliberately: this is a personal dashboard, not infrastructure. If you're not logged in, you're not coding, and there's nothing to pace.
Manual, or as a fallback: node src/server.mjs, or double-click
Open Claude Budget.command (macOS).
Claude Code hands its status-line command your real /usage percentages.
scripts/statusline.mjs forwards them to the dashboard, which then calibrates
the weekly and 5-hour limits and both reset times on its own. Point
statusLine in ~/.claude/settings.json at it:
"statusLine": {
"type": "command",
"command": "node /path/to/yet-another-usage-tracker/scripts/statusline.mjs"
}On its own it prints 5h 23% · week 41%. To keep a status line you already
have, put its command after --; it receives the same input and its output is
shown unchanged:
"command": "node /path/to/yet-another-usage-tracker/scripts/statusline.mjs -- npx -y ccstatusline@latest"The reading is posted to localhost and never waited on, so a stopped
dashboard costs the status line nothing beyond a 300ms connect timeout. Set
BUDGET_PORT in the command's environment if the server runs on another port.
The real percentages reach only the status line. There is no usage API to
call, and /usage can't be automated — claude -p "/usage" returns only the
sentence "You are currently using your subscription to power your Claude Code
usage". What Claude Code (v2.1.80+) does do is pass its status-line command a
rate_limits object: five_hour and seven_day, each with used_percentage
and resets_at, for Pro and Max, once a session has had its first API
response (docs).
The status-line hook above turns those into calibration.
They are two numbers, though, not a ledger: no per-day shape, and no Fable percentage. So consumption is still computed from your own transcripts as a relative weight, and the live readings set its scale:
weight = input + output×5 + cacheWrite×1.25 + cacheRead×r (×model tier, ×2 fast mode)
r is the model's cache-read ratio — 0.1 for most models, 0.05 for Opus 5.5,
0.025 for Fable 5.1. Advisor calls are recorded inside the executor's message
(usage.iterations, type advisor_message) and are weighted at the advisor's
own model, in its own pool.
Those coefficients come from API price ratios, but nothing is displayed in dollars — on a subscription there is no dollar meter, and showing API prices would be a number you never pay. Everything on screen is a share of your weekly allowance.
That's also why the estimate being imperfect doesn't matter much: pacing is scale-invariant. If the total is 20% off, every day's slice is off by the same factor, and the ratio between days — the thing that tells you whether to keep going — is unchanged.
Fable is capped at 50% of the weekly limit (documented by Anthropic), and its usage also counts toward the weekly total. Two meters, one budget.
That leaves two unknowns: the weekly limit W, and how heavily Fable is metered.
API list prices have no necessary relationship to subscription metering, so
the documented 50% is treated as fact and the weight k is solved instead,
from two readings taken at the same moment:
fable×k = fablePct × 0.5 × W
fable×k + other = weekPct × W
other is measured directly and unaffected by k, so W falls out, then k.
Solve it on your own account; one solve is one data point.
With the status-line hook, the week % is always current, so typing the Fable %
alone solves against it. A live week reading on its own re-solves W at the
current k and never touches k: pairing a Fable % read earlier with a later
week % would solve the weight against usage the Fable reading never saw.
k is one number for every Fable model, but Fable 5 and 5.1 bill cache reads at
different ratios (0.1 vs 0.025), so a k solved in a week heavy on one fits the
other less well. Recalibrate after switching.
- Inferred (default): the heaviest rolling 7-day stretch you've actually
sustained, plus 15%. Rolling, not calendar-aligned — a calendar-week max
ignores the in-progress week, which lets a heavy current week exceed its own
inferred limit and pin remaining budget to zero.
This number paces allocation, but it is not shown as a percentage: every
"% of limit" readout stays
—until a real reading is entered. Pacing only needs the week's relative shape; reporting "72% of your limit" off a guess would be fiction stated to the pixel. Because that leaves nothing on screen, an uncalibrated dashboard opens behind a blocking setup dialog that asks for the reset window and both percentages before anything renders. It has no dismiss button until a solve actually fails, so an unsolvable week can't lock you out. - Live: with the status-line hook, every changed reading solves
limit = spentThisWeek ÷ pctwith no typing. The 5-hour limit is solved the same way against the current block. - Calibrated: run
/usage, type the weekly percentage into the field. Same solve, one reading. A live reading replaces it on the next update. - Pinned: when you hit a real limit, Claude Code writes an error into the transcript. The scanner detects it and pins the limit to your exact week-to-date consumption at that instant. Free ground truth, no typing.
Each policy fixes the other's weakness:
- Baseline — every day's share is computed once from the week's weights. It does not move when you overspend, which is what makes the overspend visible instead of silently repricing later days.
- Debt — cumulative (used − baseline) through yesterday, shown as "running balance". Reported, not absorbed.
- Reserve — 15% held back all week, released when 2 days remain. Its job is to pay the debt down late, so a heavy Tuesday doesn't throttle Friday.
Remaining days get baseline + (releasedReserve − debt) × weight, with a hard
cap so the week can't exceed the limit regardless. Tunable via reserveFraction
and reserveReleaseDays in ~/.claude/budget-data/plan.json.
| With the status-line hook | Without it | |
|---|---|---|
| 5-hour block start | Observed — five_hour.resets_at |
Inferred — first activity after a ≥5h gap, rolling every 5h, or typed in |
| Weekly reset anchor | Observed — seven_day.resets_at |
Configured — nothing in the transcripts records it |
| Path | Role |
|---|---|
src/scan.mjs |
Incremental transcript reader. Caches per-file byte offsets + per-message records in ~/.claude/budget-data/scan-cache.json, de-duplicated across files at merge time. A cold scan re-reads every transcript and can take minutes on a large history; incremental scans read only appended bytes. |
src/pricing.mjs |
Per-model tiers and the cache multipliers that set relative weight. |
src/budget.mjs |
Week windowing, limit inference, buffer+debt allocation, block detection. |
src/server.mjs |
GET /api/state, POST /api/plan, POST /api/observe (status-line readings), static files. |
scripts/statusline.mjs |
Status-line command: forwards rate_limits to the server, optionally wraps another status line. |
web/ |
Dashboard. Build-once / patch-in-place rendering so drags survive updates. |
Plan and cache live in ~/.claude/budget-data/ — deleting either is safe.
BUDGET_DATA_DIR points them elsewhere, e.g. to try a change against a copy.
- Messages appearing in several transcripts (a resumed or forked session copies its parent's history — ~50% of usage-bearing messages here) are counted once, de-duplicated at merge time over a sorted file walk, so warm and cold scans agree. A message with no id can't be identified across files and is always counted.
- Only this machine's transcripts are counted. The allowance is per
account, but
~/.claude/projects/is per machine. Without the status-line hook, a laptop and a desktop each see only their own share and both understate the account-wide total. Live readings are account-wide, so with the hook the headline % matches/usageat each reading; the per-day split between readings still comes from this machine only. - The status line carries no Fable percentage, so the Fable weight still needs
a typed Fable % from
/usage. Live readings arrive only while a Claude Code session with the hook is running, and only on Pro and Max. - Cache-write TTL is assumed 5m unless the transcript carries the newer
usage.cache_creationbreakdown, so 1h-TTL writes are under-weighted. - Without the status-line hook, the weekly reset day/hour is a guess until you
set it to match
/usage. ~/Desktopis TCC-protected, so the LaunchAgent needs Full Disk Access granted to yournodebinary (or move the project elsewhere).- Re-grant Full Disk Access after
brew upgrade node. macOS attaches TCC grants to the resolved binary, and Homebrew'snodeis a symlink into a version-stamped path (/opt/homebrew/Cellar/node/<version>/bin/node). A node upgrade moves the real binary, the grant stops matching, and the agent fails silently — the dashboard just stops updating with no error or prompt. Symptom:~/.claude/budget-data/agent.errshowsEPERM. Fix: re-add/opt/homebrew/bin/nodeunder System Settings → Privacy & Security → Full Disk Access. Moving the project off~/Desktopavoids this permanently.
Issues and PRs welcome. It's a small, dependency-free codebase — keep it that way: no runtime dependencies, no build step, plain ES modules.
MIT — see LICENSE.