A local dashboard for your own Claude Code token usage.
Claude Code already writes a full JSONL transcript of every session to
~/.claude/projects. Each assistant response in those files carries a usage
block β input, output, thinking, cache-write and cache-read tokens. Claudinator
reads those files, aggregates them, and serves a single-page dashboard so you
can see where your tokens actually go: which projects, which subagents, which
models, which days, and which long-running sessions are quietly costing you a
fortune in re-read context.
Everything is local. No API key, no network calls, no telemetry. The server
binds to 127.0.0.1 and only ever reads files that are already on your disk.
It also refuses any request not addressed to localhost or 127.0.0.1 on its
own port, so a web page that points its own domain at your machine (DNS
rebinding) cannot read your session titles through your browser.
Claudinator ships no data of any kind. There are no sample transcripts, no bundled numbers and no baked-in paths β the repository is only code. Every figure you see is computed on your machine, at the moment you press FETCH, from your own transcripts, so two people running this see completely different dashboards: their own projects, their own agents, their own days.
Nothing is ever uploaded, and nothing derived from your transcripts is
committable by accident: the parse cache (.cache/), your config.json and
your compaction marks (compact-marks.json) are all gitignored.
Requires Node 18+ and nothing else β zero dependencies, no build step.
git clone https://github.com/bahuckel/Claudinator.git
cd Claudinator
node server.jsOpen http://localhost:8752. There are launchers that start the server and
open the browser for you: start.bat on Windows, ./start.sh on macOS and
Linux.
Run the test suite with:
npm testThe tests never touch your own install: each suite gets its own temporary
state folder (see CLAUDINATOR_STATE_DIR below) and removes it when done, so
your cache, marks and settings come out of a test run byte-for-byte unchanged.
The page follows your operating system's light or dark setting. While a fetch you asked for runs, the last result stays on screen, dimmed; AUTO refreshes swap it in without dimming.
Press FETCH (or the R key) to rescan your transcripts. The status line
under the header reports the window, the totals and how long the round trip
took.
AUTO (or the T key) turns on a rescan timer β 30s, 1m, 5m or 15m, your
choice β and is off by default. A hidden tab skips its scans and catches up
when you come back to it.
Pick a range with the pills: 1D / 7D / 1M / 6M / 1Y / ALL. These are rolling windows ending today, not calendar periods. Your range, chart metric, stacking mode and the collapsed state of the suggestions panel are remembered in the browser.
Total tokens, estimated cost, output (with the thinking share), cache hit rate,
cache never reused, requests and active days, plus a Fast mode card when any
turn ran at speed: "fast" and a Server tools card when web search or web
fetch was used. Where a comparable previous window exists (the 7 days before the
current 7, for example), each card also shows the change against it.
Under Total tokens a thin bar splits it into input, output, cache write and cache read. On an agentic workload cache reads are nearly all of it β the whole context re-read every turn β which is why the number dwarfs what tools counting only input and output report; the bar says so at a glance, and hovering it gives the exact shares.
Requests are API requests, not chat messages: every tool round-trip is one, so a single reply can be dozens. Under it is the number of conversations β what you would call sessions. Resuming a session starts a new transcript on disk, so there are usually more transcripts than conversations; hover the card for both.
Cache hit rate is the share of prompt tokens that were served from cache instead of being sent fresh. On long agentic sessions this is normally very high; a low number means something is invalidating the prompt prefix on every turn.
Cache never reused is the other side of that coin: cache writes that nothing read back before they expired. A 5-minute write only pays off if the same conversation asks again within five minutes, an hour write within the hour, so each write is checked against the next turn of the same conversation (subagents counted separately, since they have their own context).
If a model appears that pricing.json does not list, a banner names it and says
how much of the window was priced at the fallback rate β an unknown model
silently costed at Opus rates is how an estimate ends up 5x wrong.
Every main-thread turn re-reads the entire discussion, so a session's cost per
turn tracks its context size. Claudinator measures that context directly: for
the latest main-thread turn of each session, input + cache write + cache read
is what the model had to read to answer.
Conversations at or above compactThresholdTokens (default 150,000) are grouped
by project and listed with:
-
when the conversation began and the first eight characters of its session id, next to the title. Claude Code titles a conversation by what it is about, so a project worked on every day produces a run of cards with the same name; the date and id are what tell them apart;
-
current context per turn, against that model's context window, plus the peak it has reached. Models from the 4.6 generation on have 1M; Haiku 4.5 and everything earlier have 200K (the
contextfield inpricing.json). A peak past the documented window means the conversation ran on a larger one β Sonnet 4 and 4.5 had a 1M beta β and the meter uses that instead; -
cost per turn right now, and what share of that is pure re-reading;
-
how many times it has already been compacted, and how many of those were automatic. Claude Code writes a
compact_boundaryrecord for every compaction, saying whether you ran/compactor it had to step in itself; the count comes from those. An auto at 969.8k badge means the conversation ran into the context ceiling first, and hovering it says what each turn that close to it was costing. Transcripts from a Claude Code old enough to write no records fall back to spotting a drop of more than 50% between two turns; -
what
/compactwould save per turn, and over the next 50 turns (assuming the context lands nearcompactTargetTokens, priced at the cache-read rate); -
how much of that context is tool output, and which tool dominates it;
-
an
idle Ndbadge for conversations untouched for longer thancompactIdleHours(default 48).
Idle conversations are folded away under a closed Idle row at the bottom of
the panel, and the headline counts only the active ones: a week-old
conversation would have to be resumed before /compact meant anything. Open the
row to see them; it stays open or closed across refreshes.
π Notify me asks the browser for notification permission and then raises a desktop notification when a session first crosses the threshold, or when one already flagged has doubled its context since the last alert. It is per-browser, off by default, and only fires on a fetch β pair it with AUTO so it can check without you. Marking a session compacted resets its alert.
Repeats are collapsed onto the card rather than piling up as separate alerts. A session that has been raised more than once carries an asked NΓ badge saying how many times, and hovering it gives the date of the first one; the notification itself says Still needs /compact (3Γ) instead of arriving as if it were news. The tally clears when you mark the session compacted, or when it falls back under the threshold on its own.
Each card has a Compacted β button. Press it after you actually run
/compact in that session: the click timestamp is recorded, and from then on
only turns after it count toward the suggestion. The card disappears until
the session grows past the threshold again.
Marks collect at the bottom of the panel in a collapsed Marked compacted
dropdown β a count badge, then newest first when you open it, each row showing
the project, when you marked it, how many turns have happened since, when it
expires, and an undo that forgets the mark and measures the whole session
again. A mark is forgotten automatically after markRetentionDays (default 7),
at which point the session counts from its beginning again; expired marks are
deleted from the file, not merely hidden. Marks live in compact-marks.json
next to server.js (gitignored).
β Options slides open the four thresholds this whole panel runs on and lets you set them from the page. It is shut by default and costs no room when it is:
| Knob | Range | Default |
|---|---|---|
Suggest /compact above |
20k β 10M tokens | 150,000 |
| Measure the post-compact size | on / off | on |
| Measure from | any project with compactions | all projects |
Context left after /compact |
2k β 1M tokens | 20,000 |
| Call a session idle after | 1 β 2,160 hours | 48 |
| Keep "Compacted" marks for | 0 β 365 days | 7 |
The post-compact size is measured, not assumed. The turn straight after a
compaction is metered like any other, so its size is simply what a compacted
session weighs, on your machine, for your settings. Claudinator takes the turn
after each of Claude Code's compaction records in range and uses the
median. (The record's own postTokens is the summary alone β about 13k β
while the next turn also carries the system prompt and tools, which is what it
bills.) Without records it falls back to spotting drops, where the median
matters more: one drop is usually a context edit that happened to clear half the
window, not a compaction, and a mean would let it move the estimate by tens of
thousands of tokens. Under three observations it falls back to the number you
set by hand and says so.
The Measure from dropdown scopes that to one project, listing only projects that have actually been compacted, most evidence first, with the count beside each name. All projects is usually right: on the corpus this was built against, 47 recorded compactions put the median at 71.5k, range 60.6kβ89.8k β the figure barely moves between projects, and a single project rarely has enough events to beat the pooled one. (Spotting drops instead had found 13, one of them a 317k context edit.) The savings estimate is only as good as this number, and the old fixed default of 20,000 was low by a factor of three and a half.
The threshold slider says how many sessions in the current range it would list
before you save, so you can find the number you actually want instead of
guessing and refetching. The target is always kept at least 5,000 tokens below
the threshold, since a target that met it would claim compacting saves nothing.
Values are clamped to their range rather than rejected, Reset to defaults
puts all four back, and Save writes them to config.json and refetches β
no restart. Notifications ride on the same threshold, so raising it makes them
rarer and lowering it makes them chattier.
The panel header stays visible when collapsed, so the count is always at hand.
One column per day, with two independent toggles:
- Stack by β type (input / output / cache write / cache read), project,
model, or effort (
output_config.effortof each turn). The categorical stacks keep the top 8 keys and fold the rest intoother. - Metric β tokens, estimated cost, or output only.
Long ranges are bucketed automatically: more than ~10 weeks of days are grouped into weeks, more than ~13 months into months. Hovering any column gives the exact numbers.
- Calendar (1M and up) β a GitHub-style heatmap, one cell per day.
- Rhythm β token totals by hour of day and by weekday, so you can see when you actually work.
- π Best days β the top 10 days by total tokens, each naming the project that dominated it.
- Per project β see How attribution works below.
- Per agent β the main thread versus each subagent type.
- π§ Tool output β how many tokens each tool has poured into contexts, with call counts, the average per call, and the biggest single results. This is usually what makes a session expensive: a tool result is re-read by every turn that follows it.
- π Context growth β the exactly measured counterpart: how many tokens were appended between one turn and the next, and the largest jumps with the tools that ran in them.
- Agent runs β every individual subagent launch, by its task description.
- Per model β tokens and cost split by model, shown by name ("Opus 5.5", "Sonnet 3.5") rather than id; hover a name for the exact id. The chart's By model stacking and the filter chip use the same names.
- Per effort β the same split by effort level.
- Top conversations β the 25 largest, titled from the custom title or the first prompt, with when each began and its current context size. A conversation resumed across several transcripts is one row, marked +N resumed, and clicking it filters to the whole chain.
Rows in the project, agent, model, effort and conversation tables are
clickable:
one click filters the entire dashboard β every chart, KPI and panel β to that
slice. Active filters show as chips under the header; remove one with its
cross, or clear them all with Escape. The CSV export follows the current
filter.
Every column header sorts its table; click again to reverse. Each table remembers its sort across refreshes, and with none chosen it is in the server's order β most tokens first β which the header marks.
Everything works from the keyboard: Tab reaches every control and every
clickable row, with a visible focus ring, and Enter or Space on a row
filters just as a click does. R refetches and T toggles AUTO.
The CSV button downloads the daily series for the current range
(/api/usage.csv?range=β¦).
Deduplication. One record is one assistant API response. Claude Code writes
the same message.id once per streamed content block, so records are deduped on
message.id + requestId. A transcript that also exists in an archive/ folder
or a backup root is deduped the same way, so adding backup folders is safe.
<synthetic> messages β local errors, interrupts β are not API calls and are
skipped.
One conversation, not one transcript. The same resume that duplicates
records also mints a new sessionId, so a conversation resumed every day for
three weeks is dozens of transcripts on disk β and all but the last no longer
exist to run anything in. They are folded back into one conversation before
anything is counted or suggested. Two things survive a resume's copy exactly,
so no threshold is needed:
- the first message. Every copy made from the start shares its uuid.
- Claude Code's
compact_boundaryrecords./compactitself writes one into the same transcript and does not start a new one; but a resume after a compaction copies only from that point on, so it has a new first message β and carries the boundary it was copied across.
Transcripts that share either are one conversation, named for the one used
most recently. (Two windows resumed from the same point and both still in use
show as one card.) On the corpus this was built against that turned 100
transcripts into 22 conversations and the /compact panel's live cards into 6 β
a conversation worked on daily since the start of the month, 52 transcripts
long, had been showing as twelve cards under the rule this replaced, which
needed a successor to hold half its predecessor's calls and so missed every
resume after a compaction. A folded card carries a +51 resumed badge naming
the transcripts behind it.
session itself is untouched: marks, filters and drill-down stay keyed on the
real transcript id, and a mark on any session in a chain silences the whole
conversation, since you marked the conversation compacted whichever of its ids
happened to be on screen.
Which copy of a duplicate wins. Resuming a session does not continue the
old transcript: it starts a new one and copies the history in,
rewriting each copied line's cwd and sessionId to the fork's. On the corpus
this was built against, half of all API calls exist in two to eight files,
carrying 47% of the tokens β so the copy that wins decides which project
roughly half your usage is filed under.
The winner is the copy in the transcript that starts earliest, because a fork cannot predate the conversation it copied. A copy whose usage has been zeroed out never wins, whatever its age: a stub is not the record of an API call. Before this rule the winner was whichever file the directory walk reached first, which put 466M tokens (+28.5%) in a workspace bucket that belonged to the projects underneath it.
Projects. A session's cwd is often a subfolder (myrepo/src/client), so
each cwd is rolled up:
- to the enclosing git repository, if there is one (deepest
.gitwins, so nested repos stay separate); - otherwise to the shallowest folder in use that is not a workspace β a
folder that contains several distinct project subfolders in use, such as
~/Desktop/Projects; - otherwise to the cwd itself.
Claude's desktop app gives every throwaway chat a folder of its own under
<app data>/Claude/scratch-workspaces/, named after a date. Those are gathered
into one Claude scratch project rather than listed one per chat; pin one
under projectRoots if you want it kept apart.
A session started directly inside a workspace folder has no project of its own,
so its file paths are used instead: every absolute path in its tool calls
(file_path arguments, and paths inside shell commands, quoted ones included)
is mapped to a project, and if one project takes at least 60% of at least three
hits, the whole session is attributed there and the row is marked inferred
from file paths. Genuinely cross-project sessions and sessions that touched
nothing keep the <name> (root) label. Turn this off with
"inferProjectFromPaths": false.
On the corpus this was built against, that moves 15 of 17 root sessions to a
real project with a 100% one-sided signal, shrinking the meaningless
(root) bucket from 460M tokens to 4M.
The project table shows how many folders fed each project and how many of its
sessions were inferred; hover for the list. If the heuristic guesses wrong for
your layout, override it in config.json.
Paths are read as recorded, not as the machine reading them would write them: transcripts copied from Windows and opened on macOS or Linux roll up, infer and are named the same as on Windows β both separators count, and Windows paths compare case-insensitively wherever they are read.
Agents. Main-thread records are main thread. Subagent transcripts live in
<session>/subagents/agent-<agentId>.jsonl and every line carries agentId.
The spawning session holds the other half of the link: an Agent/Task
tool call (which knows the subagent_type) and the matching tool result whose
toolUseResult.agentId names the subagent. Claudinator joins the two, which is
what gives both the agent type and the human-readable task description.
Total tokens = input + output + cache write + cache read. Cache reads usually dominate, which is normal for long agentic sessions β they are also the cheapest tokens, at 10% of the input rate.
Tool output is sized from the tool result's own content β the part the
model actually reads β at roughly 4 characters per token, plus a flat 1,600
tokens per image block. Results under 400 characters are ignored. The
transcript line is not a good proxy: it carries the payload a second time
under toolUseResult plus routing metadata, so measuring the line roughly
doubles the real figure.
Context growth is exact rather than estimated. The prompt of turn N+1, minus the prompt of turn N, minus what the model itself wrote in turn N, is precisely what got appended in between β all three numbers come straight from the usage blocks. Turns where the prompt shrank (compaction, context editing) are skipped rather than counted as zero.
It is tempting to attribute that exact growth to the individual tool results in the gap, which would give exact per-tool token counts. Measured against real transcripts, it does not hold:
- Aggregated over 3,550 turn gaps, the growth implies 0.87 characters per token β impossible for text. The gap carries system reminders and other injected context, not just the tool result.
- The overhead is not a constant that can be subtracted: turns where nothing arrived still grew by 97 tokens at the 10th percentile and 2,097 at the 90th.
- Over a thousand gaps show a negative delta from context editing and compaction, which breaks attribution for those turns outright.
Exact per-result counts would need a tokenizer for the current Claude models, which Anthropic does not publish, or a call to the token-counting API, which would mean an API key and network access. Both were rejected in favour of keeping this tool local and keyless, so per-tool figures are labelled as estimates and per-turn growth is offered as the measured number beside them.
Costs are computed locally from pricing.json (USD per 1M tokens, first-party
Anthropic API rates) with the standard cache multipliers: a 5-minute cache write
costs 1.25Γ the input rate, a 1-hour cache write 2Γ, and a cache read 0.1Γ.
A model can override any of those three. They are not uniform: Fable and Mythos 5.1 and 5.2 read cache at 0.025Γ their input rate, and Opus 5.5 and Sonnet 5.5 at 0.05Γ, not 0.1Γ. Cache reads are what an agentic session spends almost everything on β 99% of the tokens on the corpus this was built against β so billing them flat overcharges those models 2β4Γ on the line that matters most. Put the exception on the model row:
"claude-fable-5-1": { "input": 10.0, "output": 50.0, "cacheMultipliers": { "read": 0.025 } }Only the named multiplier is replaced; the others fall back to the global block.
There is no long-context premium to model. Claude 4.6 and later include the full 1M token context window at standard pricing β a 900k-token request bills at the same per-token rate as a 9k one.
The one exception is Haiku 5.5, which is priced by prompt length: a request
whose prompt β every input token, cache reads and writes included β is over
100,000 tokens bills every token of that request, output too, at the higher
rate ($0.50 / $2.50 instead of $0.10 / $0.50). Each request is priced on its
own. The row says so with a longPrompt block:
"claude-haiku-5-5": { "input": 0.1, "output": 0.5, "longPrompt": { "over": 100000, "input": 0.5, "output": 2.5 } }Matching a model to a rate goes in three steps, so a name the table has never seen still gets a sane number:
- the exact
modelsentry; - the same name with a dated snapshot suffix stripped, so
claude-sonnet-4-5-20250929prices asclaude-sonnet-4-5; - the
familiesblock βfable,mythos,opus,sonnet,haikuβ matched against the name, each priced as that family's newest model, and only then the flatdefault.
Step 3 matters because default is a single number: whichever tier it names,
every model in the other tiers is wrong by that much. An unlisted Haiku priced
at an Opus-tier default comes out 50Γ over list. Anything that misses step 1
is still named in the dashboard's unknown-model banner with what it cost β a
guessed rate is a guess, however close.
The shipped table carries Claude 3 through 5. Rates change; edit the file.
Fast-mode responses use the fast rates where a model defines them, and server
tools are billed per 1,000 requests from the serverTools block (web search
$10/1k; web fetch has no per-request fee β its content arrives as input tokens
and is already counted).
This is an estimate, not a bill. A Claude subscription is not billed per
token, and published rates change. Edit pricing.json to match your own
numbers β it is re-read on every fetch, so no restart is needed. A typo there
does not fail loudly (a row with "inptu" prices every call at $0), so
npm test checks every row: known keys only, positive rates, output above
input, fast mode and the long-prompt rate not cheaper than standard,
multipliers in (0, 2], and a display name for every listed model. It also
prices every model on Anthropic's current price list β input, both cache
writes, cache read, output and fast mode β against the published figures.
Everything works with no configuration. The four /compact thresholds are
editable from the page itself β β Options in the suggestions panel writes them
to the same file β so the only reason to open config.json by hand is the
rest of the keys. Create it next to server.js (it is gitignored); all keys
are optional:
{
"roots": ["~/.claude/projects", "D:/backups/claude-transcripts"],
"port": 8752,
"workspaces": ["~/Desktop/Projects"],
"projectRoots": ["~/code/monorepo"],
"minWorkspaceChildren": 3,
"compactThresholdTokens": 150000,
"compactTargetAuto": true,
"compactTargetScope": "*",
"compactTargetTokens": 20000,
"compactIdleHours": 48,
"markRetentionDays": 7,
"inferProjectFromPaths": true
}| Key | Meaning |
|---|---|
roots |
Transcript folders to scan, recursively. Defaults to ~/.claude/projects. Duplicates across roots are dropped. |
port |
HTTP port. Default 8752. |
workspaces |
Folders to always treat as containers of projects rather than projects. |
projectRoots |
Folders to always treat as a single project, even without .git and even when many subfolders are in use. |
minWorkspaceChildren |
How many distinct subfolders in use make a folder a workspace automatically. Default 3. |
compactThresholdTokens |
β Context size at which a session earns a /compact suggestion, and at which it is notified about. Default 150000. |
compactTargetAuto |
β Measure the post-compact size from your own compactions rather than using compactTargetTokens. Default true. |
compactTargetScope |
β Project whose compactions to measure, or "*" for all of them. Default "*". |
compactTargetTokens |
β Context size after compaction when measuring is off, or when fewer than three compactions have been seen. Kept at least 5,000 below the threshold. Default 20000. |
compactIdleHours |
β Conversations idle longer than this are folded under Idle and left out of the headline count. Default 48. |
markRetentionDays |
β Days a "Compacted β" mark survives before it is deleted. 0 keeps them forever. Default 7. |
inferProjectFromPaths |
Attribute workspace-root sessions to a project using the files they touched. Default true. |
Keys marked β are the ones β Options edits; the rest are file-only. A save
rewrites only those keys and leaves everything else in the file untouched. If
config.json is not valid JSON the save is refused rather than overwriting
whatever you had in there.
CLAUDINATOR_ROOTS (path-delimiter separated) and PORT override the file.
Both are read at boot; everything else is re-read on each fetch, so edits β
from the page or from an editor β apply without a restart.
CLAUDINATOR_STATE_DIR moves everything Claudinator writes β the parse cache
(.cache/), compact-marks.json and config.json β to another folder. The
default is the folder server.js is in. pricing.json is not state and stays
with the code.
The page is a thin client over these endpoints:
| Endpoint | Returns |
|---|---|
GET /api/usage?range=1d|7d|30d|180d|365d|all |
Everything the page shows, as JSON. Optional project, agent, model, session (one transcript), conversation (a whole resumed chain), effort filters. |
GET /api/usage.csv?range=β¦ |
The daily series as CSV. |
POST /api/compact-mark |
Body {"session":"β¦","ts":1757000000000} records a compaction mark; {"session":"β¦","clear":true} removes it. Refuses cross-origin writes. |
GET /api/settings |
The editable settings, their defaults, and the spec the page builds its controls from (type, bounds, step, label, help). |
POST /api/settings |
Body {"values":{"compactThresholdTokens":100000}} writes those keys to config.json. Clamps to range, refuses unknown keys and cross-origin writes. |
GET /api/health |
Liveness, version, boot time, the mtime of the loaded code, the configured roots and the pid. |
Transcripts are read line by line with readline, so a file is never held in
memory whole β the parser is unbothered by multi-gigabyte transcript folders.
Parsed files are cached in .cache/records.json, keyed on each file's size and
mtime, so only changed transcripts are re-read and the cache file is only
rewritten when something actually changed. A cold scan of ~90 MB of transcripts
takes about half a second; warm scans are around 10 ms.
Each fetch reports where its time went: the footer shows the scan (reading
files, mostly cached) and the aggregate (everything computed from them)
separately, and /api/usage sends the same as a Server-Timing header, which
browser devtools display beside the request. On the corpus this was built
against the aggregate for a month takes about 100 ms.
server.js static file server + JSON/CSV API
lib/scan.js discovery, parsing, caching, project/agent attribution,
aggregation, /compact analysis
pricing.json per-model rates and cache multipliers
compact-marks.json your "I compacted this" timestamps (created on demand)
public/index.html page structure
public/styles.css dark theme
public/app.js SVG charts, tables, interactions (no libraries)
test/scan.test.js parsing, attribution, pricing and aggregation tests
test/server.test.js live HTTP tests against a real server on an empty root
start.bat Windows launcher
start.sh macOS / Linux launcher
"No data in this range." Either the range predates your transcripts, or the scan found no files. Check the footer line: it reports how many files and bytes were scanned and from which roots.
Wrong project grouping. Set workspaces / projectRoots in config.json,
then press FETCH β no restart needed for those, but a restart is needed if you
change roots or port.
Nothing loads at all. Check the terminal running node server.js for the
listening line, and make sure nothing else holds port 8752.
MIT β Β© 2026 Bahuckel.
Made by Bahuckel, perfected with Claude.