Two tiny, fast tools that watch your Claude Code spend in real time.
Both apps tail ~/.claude/projects/**/*.jsonl (recursively, including
subagent transcripts) via OS-native file events, dedupe by
messageId:requestId, and apply the same LiteLLM pricing table that
ccusage uses. Same JSONL in β
same dollars out, to the cent. Codex spend is counted the same way β priced
from that same LiteLLM table β if ~/.codex/sessions exists. Grok spend is
counted too, if ~/.grok/sessions exists β but its dollars come straight
from the figure xAI reports per turn, not from a pricing table (see
Multiple sources & grouping).
| Go TUI | Mac menu bar | |
|---|---|---|
| Surface | Full-screen terminal dashboard | macOS menu bar item + popover |
| Best for | Power users, SSH sessions, scripting | "Glance and go" β always-on indicator |
| Languages | Go (single static binary) | Swift / SwiftUI (.app bundle) |
| Platforms | macOS Β· Linux Β· Windows | macOS 13+ on Apple Silicon |
| One-shot mode | claudecounter --once Β· --phases Β· --report |
(use the TUI) |
| Persists between runs? | No | Yes (~/Library/Application Support/...) |
| Live updates | fsnotify-driven | FSEventStream-driven |
| Vendors counted | Claude + Codex (both priced from LiteLLM) + Grok (vendor-reported $) | Claude + Codex (both priced from LiteLLM) + Grok (vendor-reported $) |
Pick the one that fits your workflow β they're independent, run side by side without conflict, and produce identical numbers.
Both apps ship together on a single
GitHub Releases
page. Tags shaped vX.Y.Z mean "version X.Y.Z of the project as a whole" β
each release contains all 6 cross-platform TUI binaries plus the
macOS menu bar app .zip.
Go TUI β pick your platform:
# macOS Apple Silicon
curl -L -o claudecounter \
https://github.com/jverhoeks/claudecounter/releases/latest/download/claudecounter-darwin-arm64
chmod +x claudecounter && ./claudecounterAvailable in every release: darwin-arm64, darwin-amd64,
linux-amd64, linux-arm64, windows-amd64.exe, windows-arm64.exe.
Mac menu bar app:
# Replace v1.0.0 with the latest tag on the Releases page
VERSION=v1.0.0
ZIP="ClaudeCounterBar-${VERSION}-macos-arm64.zip"
curl -LO "https://github.com/jverhoeks/claudecounter/releases/download/${VERSION}/${ZIP}"
ditto -xk "$ZIP" /Applications/
# Strip Gatekeeper quarantine β the build is ad-hoc signed, not yet
# notarized (see macapp/README.md "About signing" for context)
xattr -dr com.apple.quarantine /Applications/ClaudeCounterBar.app
open /Applications/ClaudeCounterBar.appOut-of-cycle macapp patches are also published under
macapp-vX.Y.Ztags for fixes that don't warrant rebuilding the Go TUI. Either tag namespace works for the menu bar app.
Or build from source β see the Quick start below.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Today $34.87 Month $5,676.51 β
ββββββββββββββββ by model βββββββββββββββββββββββββββββββββββββ€
β claude-opus-4-6 $3,624.89 64% β
β claude-opus-4-7 $1,932.20 34% β
β claude-sonnet-4-6 $88.66 2% β
ββββββββββββ by project (this month) β main Β· sub βββββββββββββ€
β project1 $2,385.96 main $1,421.71 Β· sub $964 β
β project2 $2,176.17 main $1,900.02 Β· sub $276 β
ββββββββββββββββ live βββββββββββββββββββββββββββββββββββββββββ€
β 10:21:14 project1 opus +$0.062 (sub) β
β 10:21:09 project1 opus +$0.041 β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Full TUI docs: tui/README.md
By default there is nothing to configure: both apps read ~/.claude/projects
and report it as one series. If you run two Claude subscriptions on the same
machine β a work account and a personal one, say β point each at its own
transcripts directory and the two are counted, and shown, separately.
Why the root path is the only knob. A session JSONL carries no account
identifier at all. The one place an identifier does live, ~/.claude.json,
is machine-global β it reflects whichever account happens to be logged in
right now, not which account produced last Tuesday's transcripts. The only
durable signal left is where the files live: each install writes its own
transcripts under its own ~/.claude/projects, so a distinct root is the one
thing that reliably tells two subscriptions apart.
# ~/.config/claudecounter/sources.toml
[[source]]
vendor = "claude"
label = "personal"
root = "~/.claude/projects"
[[source]]
vendor = "claude"
label = "work"
root = "~/work-claude/projects"- No file β today's behaviour, unchanged. With no
sources.toml, both apps fall back to exactly one implicit source (claude/claude, rooted at~/.claude/projects) β an existing user sees no change at all. (vendor, label)is the series identity, renderedvendor/label(e.g.claude/work). A label only has to be unique within a vendor βclaude/personalandgrok/personalare two distinct, legitimate series.- Duplicate or nested roots are rejected at load, not silently merged: two entries pointing at the same directory, or one nested inside the other, would double-count every event under both, so loading fails with an error naming the two sources and the shared root instead of quietly producing a wrong total.
~expands to$HOME.vendormust beclaude,codex, orgrokβ all three have a reader now. Claude's and Codex's dollars are both priced from the LiteLLM table; Grok's are vendor-reported β the number xAI computed for that turn, used as given rather than derived from any pricing table.- Codex and Grok are both auto-discovered with zero configuration. If
~/.codex/sessionsor~/.grok/sessionsexists, it becomes acodex/codexorgrok/groksource automatically, the same way the implicit Claude root works, even with nosources.tomlat all. Either never appears if its directory doesn't exist, so a machine that has never run that CLI sees no change. codex-auto-reviewbills at GPT-5.6 Luna rates. Codex's transcripts name this modelcodex-auto-review, which has no LiteLLM pricing row of its own; this tool prices it atgpt-5.6-luna's LiteLLM rates instead (Codex auto-review runs on GPT-5.6 Luna under the hood) while still displaying it under its own name,codex-auto-review, everywhere in the UI.
A missing root is not the same as a broken one. A root named in
sources.toml that simply doesn't exist on this machine (e.g. a
work-only profile that isn't installed on your personal laptop) is
silently skipped β that's the normal "not on this machine" case. A root
that exists but can't be scanned (permission denied, a dropped network
mount, β¦) is also skipped, but comes with a warning, since a confident
total that quietly omits a broken subscription would be worse than no
total at all. Contrast a typed --root that doesn't exist: that has no
legitimate "not on this machine" reading, so it stays fatal exactly as it
was before --sources-config existed β the only plausible explanation for
a typed path that isn't there is a typo.
Every cost view can collapse its rows along four axes without re-scanning anything β grouping only changes how already-counted totals are displayed, so all four modes always sum to the same grand total:
| Mode | Collapses to |
|---|---|
model (default) |
one row per model, merged across every source β today's behaviour |
vendor |
one row per vendor (claude, codex, grok) |
source |
one row per configured subscription, e.g. claude/work vs claude/personal |
total |
a single row |
In the Go TUI, press v to cycle model β vendor β source β total β model
(views 1/2/3; the active mode is bracketed in the group: line, and
the breakdown below it is of today's spend β it sums to the Today
figure on the header line above it, not Month):
Today $334.50 Month $5,209.80
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
group: model vendor [source] total (v)
claude/work ββββββββββββββββββββββββ $210.40 63%
claude/personal ββββββββββββββββββββββββ $124.10 37%
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The menu bar app has the same four modes as a segmented control above its by-model/vendor/source/total table in the popover β that table is scoped to this month, not today, matching its existing "By model Β· month" label.
claudecounter --sources-config ~/work/sources.toml # non-default sources.tomlAn explicit --root still overrides the configured list entirely, with a
single implicit source rooted there β the override --root has always had,
kept for anyone who hasn't adopted sources.toml yet. The macapp has a GUI
editor for the same file β see macapp/README.md.
xAI only recently added a usage object to Grok's turn_completed events,
so a Grok month can mix turns that carry real cost data with older ones
that don't. Both apps tally, per vendor, how many of this month's turns had
usable usage; once that fraction drops below 95% the affected row gets a
dimmed ~NN% suffix instead of being shown as if it were complete:
grok/personal ββββββββββββββββββββββββ $12.40 8% ~20%
Read ~20% as "this figure is a floor, not a total" β only a fifth of
this vendor's turns this month reported a cost, so the real number is
higher. The TUI shows it inline on every grouping mode's rows
(renderSeries's coverage suffix); the menu bar's by-model/vendor/
source/total table shows the same ~NN% next to any row it applies to. A
row spanning several vendors (e.g. total) takes the worst coverage
among its contributors, so a large complete Claude figure can never hide a
small partial Grok one. Claude never carries this marker β it emits no
coverage events and is complete by definition.
Coverage genuinely varies by when the turns happened: this project's own
corpus had usage on roughly 20% of July 2026 turns versus 92% of August
2026 turns, since xAI rolled the field out mid-month. A live check on the
machine this was written on found 118 of 134 Grok turns so far this month
carrying usable cost (88%) β recent activity is trustworthy; older months
undercount and will always show the marker.
Press 4 in the TUI (or run claudecounter --report) for a per-repo
view that puts what you spent beside what you produced β commits,
+/β lines, and files β bucketed over a window, with $/commit and
$/line ratios:
sqlengine $4,016.58 Β· 440 commits (mine) / 446 all Β· +269,230 β15,578 Β· 2,527 files
bucket $ commits(m/all) +lines -lines files $/commit $/line
2026-W19 $1,243.81 108 / 108 39,593 4,486 546 $11.52 $0.03
2026-W20 $980.22 96 / 101 21,140 3,002 410 $10.21 $0.05
In the TUI: d/w/m switch the bucket (day/week/month), [/]
cycle the window (30/90/180 days), and β/β PgUp/PgDn g/G
scroll the report (it can run to many repos Γ buckets). The view is
computed lazily the first time you open it, with a spinner while git is
collected.
CLI (non-interactive):
claudecounter --report [--days 30|90|180] [--bucket day|week|month] # human table
claudecounter --csv [--days 30|90|180] [--bucket day|week|month] # CSV to stdoutAlongside cost, each bucket also shows $/commit, $/line (to 4
decimals β per-line cost is fractions of a cent), tok/commit and
tok/line (token volume per commit/line, with k/M suffixes), plus the
repo's total token volume.
--csv prints one row per repo+bucket
(repo,bucket,usd,commits_mine,commits_all,added,deleted,files,usd_per_commit,usd_per_line,tokens,tokens_per_commit,tokens_per_line)
β undefined ratios are empty cells β so you can pipe it into a sheet or
awk. Progress goes to stderr, so --csv β¦ > out.csv stays clean.
It maps each Claude project's working directory to its git repo
(git rev-parse --show-toplevel), so worktrees and subdirs of one repo
merge together; non-git projects are skipped.
Read the ratios as a rough guide, not a measurement. Nothing in the
transcripts links a commit to a Claude session β the report simply places
spend during a window next to git activity during the same window.
Spend often produces no commits (debugging, reading), and commits happen
without Claude. +/β lines are shown separately (a single lockfile
commit can be +30k lines), merge commits are excluded, and $/commit
uses your own commits β the per-repo user.email β while the
all-authors count is shown alongside. PR/MR counts are not included yet.
Set a USD ceiling and see how close you are to it, alongside the plan limits Codex and Grok report for themselves:
# ~/.config/claudecounter/limits.toml
[limits]
daily = 50.0
weekly = 250.0
warn_pct = 80ββ short window
claude daily ββββββββββ 78% $39.00/$50.00
codex 5h ββββββββββ 92% β» 2h14m
ββ weekly
claude wk ββββββββββ 52% $130.00/$250.00
codex 7d ββββββββββ 100% β» Mon β
grok wk ββββββββββ 14% β» Thu
Rows are grouped by rough duration, but a group title is not a shared window definition. The weekly group holds three different weeks: an ISO MondayβSunday week for your USD budget, Codex's rolling 7-day window, and Grok's Thursday-20:00-UTC billing period. Each row shows its own window, and they will legitimately disagree β that is expected, not a bug.
Two kinds of number appear side by side, and the right-hand column tells
them apart β a budget row shows $spent/$limit, a plan row shows when it
resets:
| Source | Where it comes from | Unit |
|---|---|---|
claude |
your limits.toml and this tool's cost maths |
USD |
codex |
~/.codex/sessions/**/*.jsonl, vendor-reported |
% of plan |
grok |
~/.grok/logs/unified.jsonl, vendor-reported |
% of plan |
Grok reports no short window, so it simply has no row in the short-window group β it appears only under weekly. Claude is the reverse β it has a dollar figure but publishes no utilisation percentage locally.
Correction. An earlier version of this section said Grok can never carry a dollar figure, because its transcripts log cumulative context size rather than billable tokens. That was wrong. It was concluded from
_meta.totalTokens, which is cumulative context β but the same files also emitturn_completedevents carrying a fullusageobject with a per-model breakdown and a directly reportedcostUsdTicks. Grok in fact has the richest local data of the three vendors, and its per-model spend now IS wired into the monthly breakdown above (see Multiple sources & grouping), using that reported dollar figure as given rather than a pricing table. The gauges in this section are unaffected β Grok publishes no short- or long-window budget locally, so it still shows only a plan percentage here.
Codex can be short a row too: newer Codex CLI builds sometimes report
only the 7-day window and omit the 5-hour one, so a missing codex 5h
row is a legitimate vendor state, not an error.
warn_pct (default 80) colours a row's percentage amber at that
threshold and red at 100% (plus a trailing β once a row hits 100%). A
plan row's whole bar recolours the same way; a budget row's bar instead
keeps a fixed colour per vendor (today just claude; the renderer is
built to stack a second segment, e.g. a future Codex USD figure,
without changing colour behaviour) and carries the threshold signal on
its percentage text only. A window whose reset time has already passed
renders dimmed and labelled stale, and is excluded from the menu-bar
glyph's escalation; only plan (Codex/Grok) rows can go stale, since a
budget row is always evaluated against "right now".
claudecounter --limits # one-shot gauge block
claudecounter --limits --limits-config PATH # non-default configPress 5 in the TUI (or run claudecounter --safety) for a per-project
view of which permission modes your sessions ran under β and how much of
your work happens with permissions bypassed
(--dangerously-skip-permissions):
β 1673 turns (60.5%) ran with permissions bypassed, in 7 project(s)
project turns sess default accept plan auto dontAsk BYPASS container? entry
terraform-provider 195 8 2% Β· Β· Β· Β· 98% no sdk-py
data-platform 782 89 10% 1% Β· 6% Β· 83% no cli,sdk-py
Every real prompt turn in the transcripts carries a permissionMode
(default, acceptEdits, plan, auto, dontAsk, bypassPermissions),
so the percentages are exact. The container? column is different: the
transcripts have no hard docker/container signal, so it's a cwd-path
heuristic β a session whose cwd doesn't follow the host's home-dir
convention (e.g. /workspace, /app, /root on a macOS host) is marked
likely. Read it as a hint, not a fact. Note that container sessions only
show up at all when the container's ~/.claude is volume-mounted to the
host. --safety --csv exports raw per-mode counts.
Deterministic, stats-only cousins of arx's
session reports β no LLM judging, just what's measurable in the transcript.
They operate on one session (default: the most recent; pick another with
--session <id-prefix>), including its Task-tool subagent transcripts:
claudecounter --scorecard # tool calls & failure rate per tool,
# files Read 2+ times, tokens by category
# + USD, peak context size, mode history
claudecounter --timeline # chronological audit log: every tool
# call (ok/ERR), permission-mode change β ,
# and priced assistant turn
claudecounter --timeline --session 14a8997f06-10 11:44:34 mode (start) β default
06-10 11:44:53 Bash go test ./... ok
06-10 11:45:14 Edit tui/internal/ui/view_report.go ERR
06-10 11:45:24 turn opus +$0.41 (sub)
Turn counts and token sums reuse the counter's messageId:requestId
dedupe; tool calls dedupe by tool_use block id. Models missing from the
pricing table are flagged as unpriced rather than silently counted as $0.
Press claudecounter --phases for a full breakdown of where your monthly Claude Code spend goes β by subagent phase, language, model tier, project, and spawn depth β plus an orchestration token analysis that surfaces how much of each session is spent on context re-reads rather than actual work.
claudecounter --phases # this month's breakdown (civil month scope)June 2026 Β· total $6,687 Β· main $5,235 Β· subagents $1,452
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
By phase (subagents):
build $687.40 364 agents $1.89/agent
review $312.10 298 agents $1.05/agent
research $48.20 97 agents $0.50/agent
test $92.40 87 agents $1.06/agent
plan $62.80 47 agents $1.34/agent
other $249.10 201 agents $1.24/agent
total $1,452.00
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
By spawn depth (subagents):
depth 0 $1,310.00 876 agents $1.50/agent top-level (Task tool or Workflow)
depth 1 $142.00 219 agents $0.65/agent spawned from within a subagent
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Orchestration (main sessions) β token cost breakdown:
cache-read $3,508 67% β long context re-reads
output $680 13%
cache-write $1,047 20%
input $0.3 0%
β cache-read β₯30% β sessions are accumulating large contexts
consider breaking long sessions into shorter focused ones
By project:
project-alpha $2,407.50 89 sessions
fable-5 $435.00
opus $1,972.50
project-beta $1,578.00 61 sessions
opus $1,578.00
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Top 20 most expensive main sessions:
$407.40 project-alpha 06-12 16:14 1847rsp opus:$316 cr:$314(77%)
$309.60 project-alpha 06-10 11:44 896rsp fable-5:$309 cr:$251(81%)
$223.95 project-beta 06-18 15:47 1203rsp opus:$224 cr:$165(74%)
$150.83 project-beta 06-02 12:10 741rsp opus:$151 cr:$89(59%)
Reading the output:
| Column | What it tells you |
|---|---|
Xrsp |
Number of unique model responses (API calls) in the session β a proxy for context depth |
cr:$X(Y%) |
Dollar amount and % of that session's spend that went to cache-reads (re-reading accumulated context) |
β cache-read β₯30% |
Aggregate signal: sessions are carrying too much context across turns |
A high cr:% on an individual session (>60%) means most of what you paid was Claude re-reading earlier turns, not doing new work. Sessions with 1,000+ responses and >70% cache-read are the primary cost-reduction target β breaking these into shorter focused sessions or using /compact between tasks is typically where the biggest savings come from.
The by-phase breakdown identifies which kinds of subagent work are expensive per-agent (build at ~$1.89/agent vs research at ~$0.50/agent), while by-spawn-depth shows whether nested agents (depth 1+) are being used and at what cost.
A separate binary that scans the whole transcript corpus for token waste, tool abuse, skill overload, context overload, and loop patterns β ranking the worst sessions and projects so you can see where effort (and money) leaks. It reuses the same session parsing as the scorecard and runs entirely locally.
π Full docs: INSIGHTS.md β every flag, all finding categories, the LLM coaching tier, and the
--applyCLAUDE.md merge.
claudeinsights # ranked corpus leaderboard (last 90 days)
claudeinsights --days 30 # narrower window
claudeinsights --session 1a2b # drill into one session's findings
claudeinsights --json # full structured output
claudeinsights --csv # one row per finding
claudeinsights --session 1a2b --digest # compact JSON digest (LLM input / export)Corpus $12,352 spent Β· $272 estimated waste Β· 847 sessions
Worst sessions (top 15):
session project $ waste$ findings top finding
43b9d8b8 data-platform $880.38 $57.53 ... waste: 8 failed tool call(s) β¦
Findings are tiered by confidence. The structural tier (waste, abuse,
skill, context, loops, sprawl, model-routing) is always on. Waste $ counts only
new tokens β cache-read reuse is not waste. Results are cached under
$XDG_CACHE_HOME/claudeinsights/ so re-runs are ~25Γ faster (--no-cache /
--refresh to control).
The coaching tier is opt-in and uses your local claude -p CLI (no API
token required) to read the actual prompts and judge friction, corrections,
loops, prompt clarity, and recurring CLAUDE.md/memory candidates per
project. It runs only on the worst flagged sessions and caches every reply so
you don't re-pay:
claudeinsights --llm # coach the worst flagged sessions (~$0.10 each)
claudeinsights --llm --llm-max 5 # cap how many sessions get the LLM pass
claudeinsights --session 1a2b --llm # coach one specific session--llm also prints a ββ Top actions ββ list β the per-session advice rolled
up into a deduped, prioritized "what to change in how you work" summary.
Turn the mined CLAUDE.md candidates into real changes with --apply. It uses
claude -p to merge them into each flagged project's <cwd>/CLAUDE.md,
preserving your existing content and only appending a deduped
## Insights (auto-suggested) section. Dry-run by default β it prints a diff
and writes nothing; add --write to apply (atomically):
claudeinsights --llm --apply # show the CLAUDE.md merge diffs (writes nothing)
claudeinsights --llm --apply --write # actually write the merged CLAUDE.md filesCost-without-delivery (high-$ sessions with no commit and no PR) is checked via
git for expensive sessions. Build with make build-insights (or make build,
which now produces both binaries).
ββββββ
ββ $34.87
β click for full dashboard popover
The popover shows hero today/month numbers, an hourly chart, by-model and by-project tables, and a live tail of recent events.
β Full menu bar app docs: macapp/README.md
The menu bar app can publish its spend and usage to a Cloudflare Worker, and an M5Stack Core2 for AWS shows them on your desk with an LED alarm when a session's context window runs hot. Three screens (overview, per-model spend, usage bars) switch with the touch buttons; tapping a vendor row opens that vendor's models.
- Worker:
cloudflare/README.md - Firmware:
device/core2/README.md - Mac app: gear menu β Device displayβ¦
git clone https://github.com/jverhoeks/claudecounter
cd claudecounter
# Build the TUI binary β ./claudecounter
make build && ./claudecounter
# Build the macOS menu bar app β dist/ClaudeCounterBar.app
make macapp && open dist/ClaudeCounterBar.app
# Run both test suites (Go + Swift)
make test-allmake from the repo root drives everything. Run make help for the
full target list.
- π Real-time β file-watcher-driven, numbers tick up the moment Claude Code writes a new line. No polling.
- π§© Per-project breakdown with main vs subagent (Task tool) split.
- π Git activity & ROI (TUI only) β per-repo spend vs. commits/lines/
files with
$/commitand$/line, over a 30/90/180-day window. See the Git activity & ROI section. - π‘οΈ Permission-mode safety report (TUI only) β % of turns per project run with permissions bypassed, with a container-likely heuristic. See Permission-mode safety.
- π¬ Per-session scorecard & timeline (TUI only, CLI flags) β tool success rates, duplicate reads, token/cost breakdown, and a full chronological audit log per session. See scorecard & timeline.
- π Monthly spend breakdown (
--phases) β subagent spend by phase, language, model tier, project, and spawn depth; orchestration cache-read waste per session withcr:$X(Y%)flags. See monthly spend breakdown. - π― Token-first math β cost is derived from accumulated token counts at snapshot time, never from running float sums. No accumulation drift; daily and monthly numbers are reproducible to the cent across runs.
- πͺΆ No daemon, no Node, no Python β single Go binary or a single
Swift
.app. Both watch~/.claude/projectsdirectly. - πΎ Zero-config β defaults work; pricing falls back to a baked-in
table when no
pricing.tomlis present. - π Custom pricing via
~/.config/claudecounter/pricing.toml(TUI), the same file the menu bar app picks up too, plus an in-app refresh that fetches LiteLLM directly.
Each line in a Claude Code JSONL is an assistant turn with a usage
object. Both apps:
- Recurse the projects directory (subagent transcripts live two levels deeper β see "the case of the missing files" below).
- Dedupe by
messageId:requestId(first-seen wins). Claude Code re-serialises the same turn during streaming, so a single response can appear up to ~25Γ in the JSONL. - Filter internal
<synthetic>events that have no billable usage. - Bucket by local-day (Europe/Amsterdam etc. β uses your system timezone) so late-night sessions land on the right day.
- Sum tokens per
(day, project, model, isSubagent)cell. - Apply pricing at snapshot time only, by summing tokens first then multiplying β token math is exact (uint64 / UInt64), so the daily and monthly numbers are reproducible to the cent across runs.
The four token types (input, output, cache_creation, cache_read)
are kept separate end-to-end and only collapsed into a single dollar
column at display time.
The Swift menu bar app is a byte-for-byte port of the Go TUI's algorithm. The Swift test suite includes cross-language conformance tests that parse the same JSONL fixtures the Go tests use and assert identical token totals β that's the regression net against algorithm drift.
Early versions of claudecounter consistently undercounted busy days by
30β50% vs ccusage. The structural fixes (correct LiteLLM Opus prices,
messageId:requestId first-seen dedupe, local-day bucketing) all
helped, but a stubborn ~30% gap remained on heavy days even after the
rules matched ccusage's exactly.
The breakthrough came from running ccusage in JSON mode and diffing
each day per token category. The ratios were not uniform β input
ratios on heavy days hit 18Γ, but cache_create stayed at 1.1Γ.
That non-uniform inflation pointed away from a dedupe quirk and toward
missing data.
Sure enough: Claude Code writes Task-tool subagent transcripts to
<project>/<session-uuid>/subagents/agent-*.jsonl β two levels deeper
than regular session jsonls. The original */*.jsonl glob caught only
the top-level files. ccusage uses a recursive **/*.jsonl (via
tinyglobby) and was reading 2,544 of the 2,734 transcripts in the
tree that were skipped (93%!). On the test machine, that single
change closed the gap from ~$130/day to 6 cents on $5,676
month-to-date.
A subtler twist showed up later in the Swift port: roughly 30% of
turns appear in both the main session JSONL and one of its subagent
JSONLs (Claude Code logs the Task-tool result in both). With
first-seen-wins dedupe, scan order decides whether such a turn is
booked as "main" or "sub". Go's filepath.WalkDir visits <uuid>/
(directory) before <uuid>.jsonl (file) because . sorts after
end-of-string, so subagent files are read first β sub wins. We mirror
that walk order exactly in Reader.candidateJSONLs so both apps
attribute identically.
Big credit to ccusage as the ground-truth reference throughout this calibration. We mirror their LiteLLM pricing source, their dedupe key, and their recursive scan. Numbers should match within rounding noise on every clean comparison.
claudecounter can only count transcripts that Claude Code still keeps on
disk. By default Claude Code deletes session JSONLs 30 days after their
last activity, which silently caps how far back the longer-window views
can look β claudecounter --report --days 90|180, --phases, and
claudeinsights --days N all stop at whatever history survives.
To retain longer, raise cleanupPeriodDays in your Claude Code settings
(~/.claude/settings.json):
{
"cleanupPeriodDays": 180
}Set it to 90, 180, or higher (use a very large number to effectively
keep everything). Notes:
- The change only affects cleanup going forward β transcripts already deleted are gone, so bump this before you want the history.
- Retention is measured from each session's last activity date, not its creation date, so reopening an old session resets its clock.
- More history means a larger
~/.claude/projectstree; claudeinsights' on-disk cache ($XDG_CACHE_HOME/claudeinsights/) keeps re-scans fast.
Both apps fetch their pricing table from
LiteLLM
(the same source ccusage uses) and cache it to disk; the Claude 4.5 / 4.6 /
4.7 family (Opus, Sonnet, Haiku) is also baked in as an offline fallback if a
fetch has never succeeded. Codex's OpenAI models (gpt-5.6-sol,
gpt-5.6-luna) are not baked in β they arrive only via a LiteLLM fetch, same
as any other model update.
Upgrading from a pre-Codex build: a pricing cache written before this release predates the OpenAI rows entirely. Both apps detect that on load β by a schema marker stamped into the cache file β and refetch the table once automatically, so an existing cache never silently prices Codex models at $0 forever. No action needed on your part, beyond having network access the first time you run the upgraded app.
To override pricing, drop a TOML file at:
# ~/.config/claudecounter/pricing.toml β read by BOTH apps
[models."claude-opus-4-7"]
input_per_mtok = 5.00
output_per_mtok = 25.00
cache_creation_per_mtok = 6.25
cache_read_per_mtok = 0.50The macapp also accepts an override at
~/Library/Application Support/claudecounter-bar/pricing.toml which
takes precedence over the shared file. The macapp's "β β Refresh
pricing" menu item fetches LiteLLM and writes that file.
The TUI's --refresh-pricing flag does the same, scraping the
Anthropic docs page and writing ~/.config/claudecounter/pricing.toml.
tui/ β Go TUI (`claudecounter` binary)
cmd/claudecounter/ main, CLI reports, integration test
internal/{pricing,reader, pricing math Β· JSONL tailing Β· token aggregator
agg,watcher,ui}/ Β· fsnotify wrapper Β· bubbletea views
internal/sources/ sources.toml load + validate (multiple subscriptions)
internal/{report,gitstat}/ git activity & ROI report
internal/{safety,session}/ permission-mode report Β· per-session parser
internal/phases/ subagent phase/lang/tier scanner + session cache-read analysis
go.mod module: github.com/jverhoeks/claudecounter/tui
macapp/ β Swift menu bar app (ClaudeCounterBar.app)
Package.swift
Sources/ClaudeCounterCore/ headless library (Pricing, Reader,
Aggregator, Watcher, Cache, AppState,
Sources, Grouping)
Sources/ClaudeCounterBar/ SwiftUI MenuBarExtra + popover + sources editor
Tests/ 215 unit tests, incl. cross-language
conformance against the Go fixtures
scripts/build-app.sh assemble `.app` from the SPM exe
cloudflare/ β Worker + KV storing the desk-display payload
device/core2/ β Arduino firmware for the M5Stack Core2 for AWS
Makefile β drives all: `make build` (TUI),
`make macapp`, `make device-flash`, etc.
docs/superpowers/ β design specs and implementation plans
MIT.
- ccusage for the reference implementation and ground-truth numbers throughout calibration
- Bubble Tea / Lipgloss for the TUI
- LiteLLM for the pricing table
- fsnotify for the Go watcher
- Apple's
MenuBarExtra- FSEventStream for the macOS menu bar app