claude-usage: token counts and cost estimates from the JSONL logs Claude Code already writes
A local dashboard for tracking your Claude Code token usage, costs, and session history. Pro and Max subscribers get a progress bar. This gives you the full picture.
At a glance
- What is it?
- claude-usage turns the JSONL files Claude Code writes to disk into a local SQLite dashboard of tokens, models and cost estimates, with no API key and no third-party packages. It prices only models matching a short name filter, pulls its charts from a CDN, and cannot see server-side Cowork sessions.
- Who is it for?
- This project is the right pick for engineers who already run Claude Code locally and want token-level detail without handing an API key to a stranger's dashboard, and the wrong pick for anyone whose usage lives inside Cowork. Copy the repository, run a scan, then confirm the model names in your transcripts match the price table before you read any dollar figure.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 105 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Reading the JSONL transcripts Claude Code already writes
Claude Code writes one JSONL file per session into `~/.claude/projects/`, and each line of those files is a JSON record. Records of type `assistant` carry the counters a cost dashboard needs: `message.usage.input_tokens` for raw prompt tokens, `message.usage.output_tokens` for generated tokens, `message.usage.cache_creation_input_tokens` for tokens written into the prompt cache, `message.usage.cache_read_input_tokens` for tokens served back out of it, and `message.model` naming the model, for example `claude-sonnet-4-6`. `scanner.py` parses those files and stores the parsed rows in a SQLite database at `~/.claude/usage.db`. No account, key or network call sits between the transcripts and the numbers, which is the reason a Pro or Max subscriber can see token-level detail that a subscription screen does not spell out.
Three sources feed those files: the `claude` command in a terminal, the Claude Code sidebar in VS Code, and Dispatched Code sessions routed through Claude Code. Because the logs are written locally regardless of subscription type, one reader covers API, Pro and Max plans alike. One source is named as uncaptured, and it matters more than the other two combined, so read the gap before trusting a weekly total.
Incremental scanning across two project directories
The scanner is incremental by design. It tracks the path and modification time of every JSONL file it has seen, so a second pass over the same transcripts re-parses nothing it has already stored. That matters on a busy machine, where a week of Claude Code work leaves a lot of session files behind, and it is why the dashboard can be left running while scans repeat.
Two directories are searched by default. The first is `~/.claude/projects/`. The second is the Xcode integration directory `~/Library/Developer/Xcode/CodingAssistant/ClaudeAgentConfig/projects/`, which exists only where that integration has run, and any directory that is not there is skipped without an error. Point the scanner somewhere else with `--projects-dir`, which is the answer for transcripts on a mounted volume or under a relocated home directory. Results land in `~/.claude/usage.db`, next to the transcripts, unless the container image overrides that location.
No pip install: Homebrew, uv, pipx or a clone
Python 3.8 or newer is the only requirement, and nothing outside the standard library is imported: `sqlite3` for the database, `http.server` for the page, `json` for the records, `pathlib` for directory walking. No build step, no wheel to compile, no requirements file to satisfy.
On macOS and Linux the shortest route is Homebrew, and the tap has to exist first because installing a formula straight from a raw URL is no longer accepted:
brew tap phuryn/claude-usage https://github.com/phuryn/claude-usage
brew install phuryn/claude-usage/claude-usage
claude-usage dashboardThe `claude-usage` command then sits on your `PATH`. Without Homebrew, uv or pipx installs the same console script with no clone at all:
uv tool install git+https://github.com/phuryn/claude-usage
claude-usage dashboardCloning works wherever Python runs, and this is the only route given for Windows:
git clone https://github.com/phuryn/claude-usage
cd claude-usage
python3 cli.py dashboardWindows users substitute `python` for `python3`. The same split applies to every command after that: `python3` on macOS and Linux, and `claude-usage` in place of `python cli.py` once the packaged command exists.
Docker, the read-only mount, and the 9898 versus 8080 port
The container route clones the repository and runs a script that builds the image:
git clone https://github.com/phuryn/claude-usage
cd claude-usage
bash scripts/run-docker.shTwo mounts carry the state. Your transcript directory is mounted read-only, so the container can read sessions but cannot rewrite them, and the SQLite file goes into a named Docker volume called `claude-usage-data` that survives restarts and keeps the database out of your home directory.
The port is where the project contradicts itself. The Docker section says the dashboard opens at `http://localhost:9898`, while the `Dockerfile` sets `ENV PORT=8080`, exposes 8080 and launches the dashboard with `--no-browser`, and every other part of the project treats `localhost:8080` as the default. Treat the port the container reports as authoritative rather than the 9898 in the prose.
Two further switches exist only inside the image: `CLAUDE_USAGE_DB` moves the database to `/data/usage.db`, and `--no-browser` suppresses the attempt to open a browser, which matters in a container with no display. Neither appears in the usage documentation.
Cost tables priced per token, not per subscription
Costs come from one published list, Anthropic API pricing as of June 2026, linked from `claude.com/pricing#api`. Four columns are computed per model: input, output, cache write and cache read. `claude-fable-5` and `claude-mythos-5` sit at $10.00 per MTok input and $50.00 per MTok output, the opus generation at $5.00 and $25.00, `claude-sonnet-4-6` at $3.00 and $15.00, `claude-haiku-4-5` at $1.00 and $5.00. Cache reads are the cheapest column on purpose, $0.30 per MTok for sonnet against $3.00 for fresh input, which is why `message.usage.cache_read_input_tokens` deserves as much attention as the plain input counter.
Eligibility is decided by a name filter rather than a model catalogue. Only names containing `fable`, `mythos`, `opus`, `sonnet` or `haiku` are priced at all. Local models, unknown names and everything else are excluded from the totals and shown as not available, so a renamed or locally served model drops out of the cost view without raising anything.
One caveat is stated directly in the project: these are API prices. A Max or Pro subscriber is not billed per token, so the dollar column compares models, it does not predict an invoice.
Cowork sessions never reach the scanner
Cowork is the one source named as uncaptured, and the reason is architectural rather than a defect: those sessions run server-side and write no local JSONL transcripts, so nothing lands on disk for `scanner.py` to parse. Mix Cowork with terminal work across a week and the dashboard reports a floor, not a total, and nothing on the page marks which sessions went missing.
A second gap is worth naming. The project description promises Pro and Max subscribers a progress bar, while the interface that gets documented covers model filtering, a date-range dropdown with bookmarkable URLs, a sticky section nav and collapsible charts whose state survives a reload. No progress bar control appears in that description, so verify it renders in the build you install before arranging your workflow around it.
The rest of the page behaviour is plain: a single page on `localhost:8080` that refreshes itself every 30 seconds.
A Chart.js dashboard that needs a CDN connection
`dashboard.py` serves a single page from the standard library `http.server`, which is why no framework has to be installed. The charts are drawn by Chart.js, and Chart.js is loaded from a CDN rather than bundled. A host with no outbound network access still gets the page, the numbers and the tables, but the graphs never render, an odd half-working state for a tool built around working locally.
Two behaviours reward attention. Model filtering and the date-range dropdown write their state into the URL, so a filtered view survives a reload and can be bookmarked or pasted to someone running the dashboard on the same machine. Every chart and table collapses, and that preference is remembered across reloads, so a page tidied once stays tidy.
Binding is the part to watch. Defaults are localhost on port 8080, and both flags and environment variables move them:
python cli.py dashboard --host 0.0.0.0 --port 9000That line, or `HOST=0.0.0.0 PORT=9000` in the environment, serves the same session data to every host that can reach port 9000. No authentication is documented anywhere in the usage, so keep the bind address on loopback unless you have a specific reason to widen it.
Querying the SQLite file instead of running the dashboard
The repository is deliberately flat: three modules named cli, scanner and dashboard sit at the top level with no package directory, and `pyproject.toml` lists exactly those three under `py-modules` with the console script mapped to `cli:main`. Nothing in that layout is private. Once a scan has run, the SQLite file at `~/.claude/usage.db` holds the parsed rows, so a one-off question can be answered with `sqlite3` from a shell instead of a browser page, at no extra install cost.
The version number is not typed into the packaging file either. It is read dynamically from `scanner.VERSION`, and `tests/test_version.py` keeps that value aligned with the number the VS Code extension ships, so the published version follows the code rather than a constant that drifts.
Two facts about upkeep frame the rest. The last push was on 2026-07-10, the same day v1.5.5 appeared, with v1.5.4 and v1.5.3 landing on 2026-07-01, and the repository is not archived. The code is MIT licensed, so a fork can stay private and a vendored copy inside another tool needs no handshake with upstream, though nothing in the project describes a rollback path for a database written by an older build.
Editorial conclusion
This project is the right pick for engineers who already run Claude Code locally and want token-level detail without handing an API key to a stranger's dashboard, and the wrong pick for anyone whose usage lives inside Cowork. Copy the repository, run a scan, then confirm the model names in your transcripts match the price table before you read any dollar figure. If you take the Docker route, check the port the container actually prints, because the documentation and the Dockerfile disagree about it.
Frequently asked questions
Does claude-usage need an Anthropic API key or account?
No. It reads the JSONL transcripts Claude Code writes under `~/.claude/projects/` and needs only Python 3.8 or newer, since the runtime imports nothing beyond `sqlite3`, `http.server`, `json` and `pathlib`.
What does claude-usage show that the plan usage screen does not?
A per-model token breakdown that separates raw input, output, cache creation and cache read counts, plus per-project session history and a cost estimate computed locally from the June 2026 API price list.
Why do some models show no cost in claude-usage?
Only model names containing fable, mythos, opus, sonnet or haiku enter the calculation. Local models and unknown names are excluded from the totals and shown as not available.
Can claude-usage see Cowork sessions?
No. Cowork runs server-side and writes no local JSONL transcripts, so it never reaches the scanner, while terminal, VS Code and Dispatched Code sessions do.
Why does the Docker port in the docs differ from the container default?
The Docker section names port 9898, while the `Dockerfile` sets `PORT` to 8080 and exposes 8080, matching the dashboard default used everywhere else. Read the port the container reports rather than the number in the prose.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/phuryn-claude-usage)Was this useful?