A local usage dashboard for Claude Code. It reads the JSONL
transcripts Claude Code already writes on your machine — the same files
ccusage reads — and turns them into a single
self-contained HTML page.
One file in, one file out. No dependencies, no build step, no network requests.
node ccstats.mjs # build ccstats.html next to the script
node ccstats.mjs --serve # live dashboard on http://127.0.0.1:8743- Estimated API cost per model, with cache-write (1.25×) and cache-read (0.1×) priced separately — cache read is usually the overwhelming majority of tokens and a large share of the bill, and blending it into one rate hides that.
- A Standard / As billed switch in the header. Standard prices everything at each model's list rate, which is what any two dashboards can be compared on; As billed applies the Fast Mode premium and the Batch discount your transcripts actually recorded, to every figure on the page. Traffic whose serving mode was never recorded is carried at standard rates rather than guessed at, so the as-billed total is a floor — and the page says what share that is.
- What the cache saved — the same traffic priced with no cache at all. Cache reads and writes are both tokens you really sent, so the counterfactual bills each at the full input rate and moves nothing else. It can come out negative: filling a cache costs 1.25× or 2× and only repays on reuse, and the card says so rather than clamping at zero.
- Costliest sessions — a session is the unit you actually remember, and it is the one axis the day and model buckets cannot express, because sessions cross midnight. Named by when they ran; the session id is a one-way hash and is never shown.
- Ranges: All / 90d / 30d / 7d / this month / last month, with a month-to-date pace line.
- Today card — cost split by token class, a 24-bar hourly spend strip, per-model rows, averages per day / active hour / session / message, and a collapsible today-vs-yesterday table.
- Heatmap + daily chart with per-day tooltips, streaks, and a record-day marker.
- Models tab — a wallet card with a cost odometer, per-model token breakdowns and shares, and
a how it was served card: Fast Mode, the Batch API, and Priority Tier split out from
standard traffic, read from
usage.speedandusage.service_tierin the transcripts. - Right-click anything to copy just that scope: a heatmap day, an hour bar, a model row, a session, a stat card. Right-click the background for the whole-range summary, or export the whole history as JSON (lossless) or CSV (one row per day per model, RFC 4180, no BOM).
- Per-project spend, off by default and opt-in only — see Projects below.
- English / 한국어, light / dark, and a few easter eggs.
Pick whichever is least effort. Everything needs Node 18+ and nothing else.
Run it without installing anything
npx github:heistkit/ccusage-interface --serve --openbunx github:heistkit/ccusage-interface --serve --openDownload one file
curl -fsSLO https://github.com/heistkit/ccusage-interface/releases/latest/download/ccstats.mjs
node ccstats.mjs --serve --openDouble-click, no terminal — grab ccstats-<version>.zip from
Releases, then double-click
ccstats.cmd (Windows) or ccstats.command (macOS). Both also work as normal CLIs if you pass
arguments.
Scoop (Windows) — installs straight from the release, no bucket to add:
scoop install https://github.com/heistkit/ccusage-interface/releases/latest/download/ccstats.jsonHomebrew — every release ships a ccstats.rb formula with that release's checksum. Drop it
into a tap repo as Formula/ccstats.rb and brew install <yourtap>/ccstats.
npm — not published. Use npx above; it needs nothing installed either.
From source
git clone https://github.com/heistkit/ccusage-interface
cd ccusage-interface && node ccstats.mjs --serve --openNote that from source, geist-fonts.css must sit beside ccstats.mjs. Release builds inline it;
the repo build reads it off disk and falls back to system fonts without it.
The same dashboard, with the one job the CLI does for you handed back: finding the transcripts.
You point the browser at your projects folder, the browser parses it, and the page is built in
the tab. Nothing is uploaded — there is no server to upload to. The deploy is static files
only: no serverless functions, no runtime, no environment variables.
npm run build:web # -> public/Deploying to Vercel needs no configuration beyond the committed vercel.json — import the repo
and it runs build:web and serves public/. The same output works on any static host.
| File | |
|---|---|
public/index.html |
the drop zone: folder picker, drag-and-drop, and the parse loop lifted out of ccstats.mjs |
public/dashboard.html |
the dashboard; reads its payload from the tab's sessionStorage |
public/dashboard.tpl.html |
the same page with __DATA__ intact, fetched only when you ask to download your dashboard as a file |
public/demo.html |
the fabricated-data page |
The browser runs the same counting code as the CLI: tools/build-web.mjs lifts
createCollector() out of ccstats.mjs with Function.prototype.toString() rather than
reimplementing it, then compiles it in an empty scope and asserts it agrees with the CLI byte for
byte before writing anything. There is exactly one copy of the parse loop.
Node 18 or newer. That's it. No install step, no dependencies, no network access — not from the tool, and not from the page it generates.
node ccstats.mjs build ccstats.html next to this script
node ccstats.mjs --serve live dashboard, rescans as you work
node ccstats.mjs --serve --open ...and open it in your browser
| Option | |
|---|---|
-s, --serve |
run a local server instead of writing a file |
-p, --port <n> |
port for --serve (default 8743) |
-o, --out <path> |
output path for the static build |
--open |
open the result in your default browser |
--lan |
with --serve, also accept connections from your network (default is localhost only) |
--root <path> |
transcript folder to scan; repeatable |
--config <path> |
JSON config file (default ccstats.config.json beside the script) |
--init-config |
write a commented starter config and exit |
--projects |
break usage down by project folder name — off by default, read Projects first |
-h, --help |
full help text |
Transcript location is auto-detected from CLAUDE_CONFIG_DIR, then ~/.claude/projects, then
~/.config/claude/projects. Override with --root or roots in the config file.
--projects is the one option that changes what the tool is willing to put in a file, so it is
off by default and it fails closed.
Without it, no project information of any kind reaches the output. The field is not empty, it is not there at all, and nothing reads the working directory. That is the difference that lets someone reading an exported payload tell "this build did not collect projects" from "it did and found none".
With it, ccstats reads the cwd on each transcript record and keeps the last path segment
only — the project folder's own name. /Users/you/work/acme-billing becomes acme-billing. The
directories above it are never read into the page, so your directory layout does not travel with
the file. Bucket ids are ordinals (p0, p1), never hashes of the path: a hash would be derived
from the exact string this is meant to keep out of the file, and with the folder name printed in
plain text beside it, anyone who could guess the parent directories could confirm them against it.
Be clear about what a folder name still gives away. It is often the name of a client, an employer,
an unannounced product, or an acquisition, and the page prints it next to what you spent there.
And one case the rule does not protect: if you ran Claude Code straight from your home
directory, the last segment is your username; a drive root lands as C:. A page built this way
says so in its header, so you can tell before you share it — but check the list first.
ccstats reads usage metadata only: timestamps, model names, token counts, and a truncated
hash of each session id. Message content and prompts are never read, at any setting. Nothing
identifying your projects reaches the output unless you pass --projects — see above. It makes no
network requests, and neither does the page it generates — fonts and data are inlined, so the
output works from file:// and offline.
The generated ccstats.html does embed your numbers, though, so it is gitignored here. If you
share one, know that you are sharing your day-by-day token counts and estimated spend.
The hosted version holds to the same line. Your files are read by the browser itself, the page
declares default-src 'none' with no outbound channel, and the deploy has no backend to receive
anything — so you can confirm it in the network tab rather than take our word for it. The
Send feedback action is a link, not a submission: it opens GitHub's issue form with a template
already in it, and the report exists only once you have read it over and pressed submit there.
Prices are hardcoded list rates per model, applied to the token counts in your transcripts.
Subscription allowances and negotiated discounts are not modeled, so this is not a bill — it
is what the same work would cost at API list price. Override rates via pricing in the config
file if yours differ.
Which rates apply is your choice, in the header. Standard prices every figure at each model's list rate. As billed applies the Fast Mode premium (2×) and the Batch API discount (0.5×) that your transcripts recorded, to every figure on the page — headline, cards, hourly strip, model rows, tooltips, copied text and the CSV. Priority Tier has no published flat multiplier, so it gets its own row but is costed at standard rates rather than being given an invented number, and requests whose serving mode was never recorded are carried at standard rates rather than assumed to be standard. Both of those make the as-billed total a floor, which the page states wherever it shows one.
Token counts never move between modes. A billing multiplier inside a token count would be a fabrication, so the heatmap, the daily chart and the wallet donut are identical in both.
UI components adapted from Uiverse.io, retokenised to this palette — thanks to JaydipPrajapati1910, kennyotsu, Li-Deheng, mobinkakei, Na3ar-17, OliverZeros, RiccardoRapelli, SelfMadeSystem, Uncannypotato69, and vikramsinghnegi.
Typeface is Geist by Vercel, licensed under the
SIL Open Font License 1.1 and inlined as base64 in
geist-fonts.css.
MIT — see LICENSE. The bundled Geist font files remain under the SIL OFL 1.1.