How much of your AI coding subscriptions have you actually used?
A macOS menu bar widget for Claude Code and Codex — live quota bars, reset times, and what your usage would cost at published API rates. Computed entirely on your own machine.
▁▄█ $42/d ← always visible in your menu bar
└─────────────────────────────────────────────────────────────────┐
Auto-switch: ON │
Cost tracking: ON │
│
Accounts │
1 work (you@company.com) │
5h ░░░░░░░░░░░░░░░░░░ 0% │
7d █████████████▍░░░░ 74% resets Aug 21 (ahead) │
Opus ██████████████████ 100% (!) resets Aug 21 (ahead) │
2 personal (you@gmail.com) ● active │
5h ██████████▋░░░░░░░ 59% resets 14:50 │
7d ██▊░░░░░░░░░░░░░░░ 15% resets Aug 22 │
│
Codex (pro) │
weekly ██████░░░░░░░░░░░ 34% resets Aug 20 │
│
Cost (notional, API list prices) │
Today $41.98 │
Last 7d $310.20 ($44.31/day avg) │
Last 30d $1,284.55 │
── by model ───────────────────────── │
Opus 5 61.0M tok $31.10 │
gpt-5.6-sol 44.2M tok $10.88 │
└──────────────────────────────────────────────────────────────────┘
Subscription plans hide the two numbers you actually need: how close to the wall am I, and which account still has room. This puts both in the menu bar, for both tools, and adds a burn-rate figure so you can see a heavy week coming before it lands.
Claude Code — every account's 5-hour, weekly, and per-model quota windows as bars, with real reset times and an ahead of pace warning when you're burning faster than the window refills. Click any account to switch to it. Optional auto-switch moves you off an account before it hits the wall. Account features need claude-swap; everything else works without it.
Codex — your weekly subscription quota, read from the local rollout logs. Optionally, one live row per Codex account — see below.
Both — notional per-model cost, computed by scanning your own transcripts, and a notification the moment an account crosses 85 %, hits the wall, or comes back — once per transition, macOS and optionally Telegram.
Requires macOS 13+ and either uv or Python 3.12+.
unzip cc-usage-widget-*.zip
cd cc-usage-widget
./install.sh # creates a local venv — installs nothing globally
./run.sh # a bar-chart icon appears in your menu barStart at login: ./install.sh --launch-agent
Remove everything: ./uninstall.sh
The dropdown opens on two cards that answer the questions you open it for:
- Claude — the account you are on, its binding window (the one
auto-switch decides on) and when it resets (
↺ 14:50), then the next account to move to, how many rooms have headroom (4/5 room), each model fleet line (Fable 4/4) and extra-usage spend once it passes 70 %. A badge counts the slots that need you. - Codex — the login
~/.codexis using, its weekly figure (or⚠ relogininstead of a figure it cannot vouch for), thenbest:(the same accountcodex_accounts bestprints), the fleet count, the next reset and any reset credits. A reset credit you can use now is the one green line in the menu.
Below them: Needs attention (every slot or login waiting on you, with the
remedy — absent when nothing is), Claude (the active account's full bars,
then one line per other account, most headroom first), Codex (one line per
account plus its single most useful fact), recent switches and cost, then the
tools. Every reset reads the same way everywhere: ↺ 14:50 today, ↺ Thu 20:25 this week, ↺ Oct 2 10:49 later, ↺ overdue once passed. The full
bars are one submenu away (All Claude bars ▸, All Codex bars ▸).
Settings ▸ Theme (menu_theme) picks how the dropdown is drawn:
Cards (default: one rounded panel per provider, the active account large,
the others one line each, fixes as copyable cswap chips), Apple (the
language of Control Center: hero lines, section headers, symbols, two-line
rows), Dense (every account on one aligned line; the shortest menu), or the
text layouts Glance (described above) and Classic (the previous menu,
exactly). The three card themes are drawn by native views in the system font,
with rounded figures and colours chosen to stay readable on a menu tinted by
your wallpaper; clicking a Claude row switches to it, a dead Codex login opens
Log in again. A stored menu_layout_classic: true still means Classic, and
Classic is also what a machine with no Claude accounts and no live Codex row
shows under the Glance theme. Settings ▸ Title ▸ Merge alerts into ⚠N
(title_merge_alerts, off) folds C⚠ and the bare ⚠ into one count that
matches the Needs attention section, which makes the title narrower. When the
Codex account you are on is capped and holds a usable reset credit, its
↺<time> countdown becomes a green ↺now (Reset-credit marker,
title_show_reset_credit, on). The title text is otherwise unchanged; each
part is now coloured by its severity.
With claude-swap installed, the Accounts block shows more than the bars:
- A fleet line per model window, directly under
Accounts—Fable 2/4 · next Tue 09:00: how many enabled, switchable slots still have room in that weekly window, and when the soonest full one resets. One line per scoped window your slots report; a machine that reports none never sees it. Settings ▸ Fable fleet line (Model fleet linewhen there are several) turns it off —scoped_fleet_line_enabled, on by default — and the block is then exactly what it was before. - Extra usage is real money, so it gets its own line under the account that
spends it —
extra $480.00 / $500.00 96% resets …— and aReal spend (extra usage) … · real, not notionalline in the Cost section. A notification fires when it crosses your threshold and again at the limit. - A window whose reset has passed is drawn dim, without its
(!): the number is from the previous window.(!)itself now means at the wall (100 %); red still starts at 90 %. - A slot with a standing problem is dimmed and says so once — its header
reads
⚠ relogin · last seen 3d agoor⚠ 403, and the full remedy is in the alert line at the top. A slot whose usage has been refused three times or more says why and what to run:usage forbidden (HTTP 403) since <day> · <n> failed polls — … log in as <email> and run cswap add, or cswap remove <slot> to stop polling. A slot held in upstream's429backoff with no good fetch for a day or more says so too:usage refused for N days since <day> (last error HTTP 429, upstream backoff), with⚠ 429in the title. - A
cswap disabled slot is markeddisabled, sorts last in Switch account with(disabled), and is never counted as room. - Account flips are named. A running Claude Code session that rewrites
~/.claude.jsonbehind claude-swap's back is a ghost flip; three in an hour is a login fight (⚠ ghostin the title, always), and the alert lists the remedies. A switch that was reverted before the next pass says so (switch →2 reverted to 1); when claude-swap's log shows no revert, it is a ghost flip (switch →2 reverted: ~/.claude.json rewritten to …) and counts toward a login fight. A plaincswap switchor a single ghost flip to a healthy slot no longer badges the title. The newest switch is shown inline, and the rest are under a Recent switches submenu. - The title follows the window auto-switch actually decides on. If that is
the Fable or 7-day window rather than the 5-hour one and it is at the
switch threshold, it takes the 5-hour figure's place, labelled and in its own
colour (
backup-a… 7d96% C86% ⚠ 0/4), so you can see which wall is closing. The room count (0/4) turns orange when no other account has room. A window whose reset has passed never binds the title and never costs the fleet a room. - Long aliases are shortened in the title to eight characters and
…(backup-account→backup-a…); the menu shows the full name.
Out of the box the Codex row is read from your local rollout logs. Those logs carry no account id, so the figure describes whichever login wrote them — fine for one account, useless for several (and two ChatGPT workspaces under one email are two accounts).
If you run more than one Codex login, the widget can show one row per
account, all of them live, with the login the CLI is currently using marked
· active. It is display only — the widget never switches your Codex login,
and never writes to ~/.codex.
This is the one feature that makes a network request, so it is off by default. Turning it on takes one login per account:
cd ~/.claude/cc-usage-widget # your install directory
# 1. Log in once per account, each into its own CODEX_HOME. The directory
# must exist first: codex refuses a CODEX_HOME it cannot find.
# The browser flow shows a workspace picker — pick a different workspace
# each time if two of your accounts share an email.
mkdir -p -m 700 codex-accounts/new-1 codex-accounts/new-2
CODEX_HOME=$PWD/codex-accounts/new-1 codex login
CODEX_HOME=$PWD/codex-accounts/new-2 codex login
# 2. Adopt them. Offline: reads each token's own claims, renames the directory
# to the account id, fixes permissions, registers it. No network request.
# PY is the interpreter install.sh chose - the one named inside ./run.sh.
PY=$(sed -n 's/.*exec "\(.*\)" -m cc_usage_widget.*/\1/p' run.sh)
"$PY" -m cc_usage_widget.codex_accounts adopt
# 3. Name them, then switch the feature on.
open codex_accounts.json # set "alias" per account: "work", "personal", …Then in the menu: Settings ▸ Codex accounts — tick the accounts you want, and turn on the live quota. Useful checks:
"$PY" -m cc_usage_widget.codex_accounts list # registry + which login ~/.codex holds
"$PY" -m cc_usage_widget.codex_accounts probe work # one request, raw numbersEach command runs in its own process, so none of them interferes with the running widget.
What to expect:
- Each account is read every 5 minutes (configurable 1–60 min), spaced out and sequentially — never four requests at once.
- A row says what is wrong instead of showing a wrong number:
relogin,no access,rate limited,endpoint error,offline. A reading older than 6 hours drops its bars and keeps the reason. - Codex access tokens last about ten days. The widget refreshes them by
default, about a day before one expires; see
Refreshing tokens for the switch and its
guards. Only when a refresh cannot happen does the row say
relogin in 1d 4htwo days out, and a dead login offers Log in again… in the menu (see below). - Delete
codex_accounts.json(or untick every account) and the menu goes back to exactly what it was.
A Codex row whose login is dead (relogin, or credential unreadable) is
clickable, and says when and what: token expired Sep 20 11:13 · Log in again…
(or last reading 5d ago · Log in again… when the expiry is not known).
Settings ▸ Codex accounts ▸ Add Codex account… does the same for an
account the widget does not track yet.
Either click writes a small script, codex-accounts/login-<time>.command
(0700, paths only — no token, no email), and opens it in Terminal. It runs
CODEX_HOME=…/codex-accounts/new-<time> codex login and then
python -m cc_usage_widget.codex_login adopt --replace, which reads the id the
NEW token claims and either swaps the fresh auth.json into that account's
existing home (a relogin: alias and checkbox kept) or registers it (a new
account). The browser shows a workspace picker; pick the workspace of the
account you clicked — two of these accounts can share an email, and the adopt
step files the login under whichever account it really is. The script deletes
itself when it succeeds.
Closed the Terminal before it finished? The widget adopts any finished
new-*/auth.json on its next poll (after a 30 s settle, so it never moves a
file codex login is still writing). codex is looked up on PATH, then
/usr/local/bin, /opt/homebrew/bin, and the ChatGPT app's own copy — the
LaunchAgent has no /usr/local/bin on its PATH. By hand:
"$PY" -m cc_usage_widget.codex_login adopt --replacePlain adopt (either module) still refuses an account that is already
tracked; only --replace swaps a login in.
Ten days is not long, and four accounts means four browser logins every ten
days. The widget avoids them by exchanging the refresh token in each
auth.json for a fresh one about a day before the old one expires. The setting
that allows it, codex_refresh_enabled, is on by default since 2026-09-25,
and Settings → Refresh Codex logins automatically (shown while live Codex
quota is on) turns it off. Off means silent: no token request of any kind is
made and the widget only ever reads your credential files. There is a test
whose whole job is to fail if that stops being true.
It used to ship off, because of one unanswered question: every codex login
uses the same public OAuth client, and it is not documented whether rotating
the token for one login invalidates the others. The off default then cost more
than it protected: on 2026-09-20 every stored token expired and the Codex rows
said relogin for five days. Hand-run refreshes on 2026-09-25 rotated three
accounts cleanly while ~/.codex kept working, and four guards now keep
refresh away from everything it could break:
- The account
~/.codexis logged in as is never refreshed. The ChatGPT app owns that login. For that one account the widget instead borrows the app's own, fresher token from~/.codex/auth.json— read-only, never refreshed, never written — and the row saysvia ChatGPT app login. While the app's login is live, the widget's own copy of that account shows no countdown. The guard fails closed: the last account a read of~/.codex/auth.jsonnamed stays protected through a torn or missing file, and while an existing file has never been readable no account is refreshed. - A refused refresh is final (
invalid_grantand therefresh_token_reused/_expired/_invalidatedcodes): nothing is retried — not on the next poll, not after a restart — until you log in again and the file changes. While the stored token still works the row keeps its figures and counts down (relogin in …); once it expires the row saysrelogin. - A file the Codex CLI rotated mid-refresh is kept. If
auth.jsonchanged while the widget's request was in flight, the widget throws its own result away and uses what the CLI wrote. - Nothing about a token is logged. Log lines carry the alias, the outcome and how long the new token lasts.
And the grant itself stays narrow:
- a refresh is attempted when a token is within 24 hours of expiring, and
exactly once more if the API answers
401— never twice in one poll; - a
403never triggers a refresh (that is an answer about permissions, not about the token); - the rotated tokens are written to disk, atomically and
0600, before the new access token is used for anything, so a crash costs one grant and never your login; - the old refresh token is overwritten in place — no backup copy, nothing that could be re-sent by accident;
- any other failure changes nothing: your stored token is still valid until it
expires, and the
relogin in 1d 4hcountdown comes back so the deadline is not hidden. While refresh is working that countdown stays away, because nobody needs to do anything.
To see one refresh happen end to end, on an account you choose:
"$PY" -m cc_usage_widget.codex_accounts probe-refresh gmailThat performs one refresh on that account, writes the rotated tokens into
its auth.json before using them, and then makes one usage request to prove
the new token is accepted. It prints the plan, email and expiry before and
after — never a token — and exits non-zero if either half fails. It refuses
the account ~/.codex is logged in as.
The widget knows the moment an account crosses a line; by default it tells you about it once, in Notification Center, and never again for the same transition.
It notifies on:
| a window crossing 85 % upward | Codex vlad · weekly 87% · crossed 85% |
| a window reaching 100 % | Codex vlad · weekly 100% · resets Sat 09:00 |
| a window coming back (below 50 % after having been at or over the threshold) | back: vlad 0% weekly |
a row that needs attention (relogin, no access, offline, endpoint error, out of credits) |
the row's own wording |
| a claude-swap sentinel, or a self-audit that found drift | the note itself |
a Codex login within 24 h of expiring while its row shows the relogin in … countdown |
Codex vlad · login expires Sep 20 11:13 · Log in again from the menu |
| Claude extra-usage spend crossing the threshold, and again at its limit | Claude main · extra usage $480.00 / $500.00 (96%) · crossed 85% of the extra-usage limit |
a model with no published rate reaching unpriced_alert_min_tokens today (default 1,000,000; 0 = off) |
Usage Bar · unpriced model · Codex gpt-… · 2.0M tokens today, counted at $0 (no published rate) |
| a Codex reset credit becoming usable | Codex reset available · vlad: 1 reset credit usable now — weekly 87% |
Once per transition, not once per tick: a standing condition notifies once and
is only re-armed when it stops being true, so an account that sits at 100 % for
four days produces one notification, and the next time it walls it notifies
again. The ledger is notify_state.json; deleting it costs at most one repeat.
One exception: a standing Codex warn (a dead login, no access, offline) is
sent again once a day while it stands — a dead login announced once and then
never again is how four rows once sat on relogin for five days. Claude
sentinels are not repeated. Every notification actually delivered leaves one
line in logs/widget.log: notify: sent <key> (the key, never the wording).
Settings ▸ Notifications has the master switch and the threshold. The threshold
lives in settings.json as notification_threshold_pct (50–100).
Off until you configure it, and it is the only thing here that touches the
network. The bot token is read from an environment variable, never from the
command line, so it cannot land in your shell history or in ps:
cd ~/.claude/cc-usage-widget # your install directory
export TELEGRAM_BOT_TOKEN='…' # from @BotFather
# PY is the interpreter install.sh chose - the one named inside ./run.sh.
PY=$(sed -n 's/.*exec "\(.*\)" -m cc_usage_widget.*/\1/p' run.sh)
"$PY" -m cc_usage_widget.notify setup \
--telegram-token-from-env TELEGRAM_BOT_TOKEN \
--chat-id 123456789
"$PY" -m cc_usage_widget.notify test # one macOS + one Telegram message
"$PY" -m cc_usage_widget.notify status # prints no secretsetup writes notify.json at 0600. The widget refuses to send from a
credential file that anyone else on the machine can read, and the Telegram
switch in Settings stays greyed out until the file is there. The token goes into
exactly one URL and is never logged, never rendered and never written anywhere
else — there is a test that plants a canary token and asserts it, plus a
companion test proving that check can fail.
Sends happen on their own short-lived thread: a Telegram request that times out cannot delay the widget's tick.
What the block tells you once more than one account is live:
- a heading —
Codex 1/4 · next Sat 09:00 (vlad)— how many logins still have weekly headroom, and when the first capped one reopens, named; out of credits · Add creditsinstead of a barecapped …when a workspace is blocked on credits rather than on a spent window, pluscredits 12andgpt-6-astra back Sep 15when the endpoint reports them;at this pace: wall in 6hon a rising account — only with three readings spanning at least half an hour, and never on a capped one;↺ 1 reset credit usable now — use it in Codexfirst on a row whose account holds a rate-limit reset credit that applies now (reset credits: 2 (not usable now)when it holds some and none applies),· 1 reset usable/· 2 resets bankedon the heading, and a one-timeCodex reset availablenotification. Display only: the widget never spends a credit;C100%↺4din the menu bar when the account you are coding on is at the wall (that component istitle_show_codex_pct, off by default).
A narrow menu bar. Set "title_compact": true (or Settings ▸ Title ▸
Compact) and the title becomes V·C 100/100 — the active Claude account's
initial and 5-hour percentage, then Codex's initial and the active login's
weekly percentage, at most twelve characters. The other title switches are
inert while it is on, and a standing problem still replaces the figure it
invalidates (V·C ⚠/100) rather than showing a percentage the account can no
longer support.
The widget also switches to the compact title by itself when the full one is
not drawn: every 30 seconds it asks the window server whether its item is
onscreen, and two hidden readings while the menu bar is visible (never in a
fullscreen app) turn compact on for the rest of that run. The setting is not
written — Title ▸ Compact reads — on for now: menu bar full — so the next
start tries the full title again.
With several logins tracked, the widget can tell you which one to start the
next session on. It is a read of the widget's own sidecar — no network request,
no switching, and nothing written to ~/.codex:
"$PY" -m cc_usage_widget.codex_accounts best # prints a CODEX_HOME path
"$PY" -m cc_usage_widget.codex_accounts best --json # + alias, weekly %, resetIt picks the enabled account with the lowest weekly usage that still has room,
skipping any that needs attention (relogin, no access, out of credits). When
every account is capped it picks the one whose window reopens first. When
nothing is usable it prints one line saying why and exits 2.
The per-account homes hold only auth.json, so a session started in one sees
none of your configuration. link mirrors it in, and it does so two different
ways on purpose:
"$PY" -m cc_usage_widget.codex_accounts link
# copied once: config.toml, memories (Codex writes to these)
# symlinked: AGENTS.md, skills, plugins, agents, rulesThe names Codex writes to are copied, never symlinked. A symlink at
config.toml would send the next codex config set inside that session
straight through into ~/.codex — the one thing this widget promises never to
do — by a write the widget does not make and could not log. Copied, each
account gets its own model, approval mode and memories, and ~/.codex stays a
file this program only ever reads. The copy happens once: a config.toml you
have since edited in an account home is that account's configuration and a
later link leaves it alone. A home still carrying a write-through symlink
from an earlier version is converted to a copy of the same file.
Everything else is linked, only when the name exists under ~/.codex, never
overwriting anything already there — so an edit to a skill or a rule reaches
every account at once. --unlink removes exactly the links it made; the copies
are the home's own files and stay.
Then put this in your shell profile:
codexb() {
local h
h=$(python -m cc_usage_widget.codex_accounts best) || return $?
CODEX_HOME="$h" codex "$@"
}Caveat worth knowing before you use it daily: Codex writes under the
CODEX_HOME you give it — sessions, history and logs land in that account's
directory, not in ~/.codex. That is the point (each account keeps its own
credential), but it means your rollout history is split across the account
homes, and ~/.codex/sessions no longer sees everything. The widget's Codex
cost figures are read from ~/.codex/sessions/**/rollout-*.jsonl, so tokens
spent in a codexb session show up in that account's quota row (which comes
from the vendor) but not in the cost block (which comes from the corpus). The
quota rows stay right either way; the dollar figures under-count by whatever
you ran on another home.
Measured against a 1.4 GB Claude corpus and a 15 GB Codex corpus:
| Idle CPU | ~0% — unchanged files are never opened |
| Memory | ~55–85 MB |
| Steady tick | ~19 ms |
| First index | seconds to under a minute, in the background — the menu never blocks |
Transcripts are read incrementally: each file is remembered by size and modification time, and only the bytes appended since last time are parsed.
Everything stays on your machine. The widget has no telemetry, no analytics, and no update check, and by default it makes no network calls at all.
There are exactly three exceptions, and you have to switch each of them on. The
first is Telegram notifications, which POST one line of
text — the same line Notification Center shows — to your own bot. The second is
the live per-account Codex quota, which
reads https://chatgpt.com/backend-api/wham/usage — your own account, with your
own login, for your own quota numbers. The third is
token refresh, which POSTs one OAuth
refresh grant to https://auth.openai.com/oauth/token for a login of yours that
is about to expire. Nothing is sent anywhere else, nothing is
uploaded, and the request contains no transcript content. Your Codex access
token is read from its file, used in that one request's Authorization header,
and dropped: it is never logged, never written to any file the widget creates,
and never shown in the menu. A credential file that other users can read is
refused rather than used. There is a test that plants a canary token, runs a
full poll cycle and asserts the canary reaches no file, no log line and no menu
label — and a companion test proving that check can actually fail.
Your transcripts contain your source code and possibly your secrets. Records are parsed as JSON — so content passes through memory, as it must for any parser — but the indexers extract only token counts, the model name and a timestamp. No prompt, completion, file content or tool output is ever stored, aggregated, transmitted, or written to any file — there is a test that plants a canary secret in a fixture and asserts it appears in none of the files the widget writes.
State files are created 0600 in the install directory:
| File | Contains |
|---|---|
rollups.json |
per-day, per-model token counters |
scan_state.json, codex_scan_state.json |
absolute path, size, offset per transcript |
codex_scan_state_quota.json |
your most recent Codex subscription quota |
scan_state_dedup.json |
request IDs seen today, for de-duplication |
codex_accounts.json |
(live Codex quota only) account ids, your aliases, order — no token |
codex_quota_snapshots.json |
(live Codex quota only) the last quota reading per account — no token |
codex-accounts/<id>/auth.json |
(live Codex quota only) one Codex login, written by codex login — and by the widget when token refresh rotates it (on by default; Settings → Refresh Codex logins automatically turns it off) |
notify.json |
(Telegram notifications only) your bot token and chat id — the one credential the widget writes |
notify_state.json |
which transitions have already been announced — no figures, no names but your own aliases |
history.sqlite |
the long-term copy of rollups.json — same counters, kept past the window |
dashboard.html |
(dashboard only) the page Open dashboard last rendered — the same counters, as charts |
backup-state-<ts>/ |
a copy of the two files above, taken before a Rebuild cost index |
settings.json |
your preferences |
logs/widget.log |
only if you use --launch-agent; trimmed to its newest half once a day past 5 MB, and a warning that repeats within an hour is written once, then as (repeated N times since HH:MM) |
If claude-swap is installed, it talks to Anthropic to read your own account quotas. That is its network activity, on your behalf, not ours.
They are notional: what your usage would cost at published API list prices. You are on a flat-rate subscription, so this is not a bill — it is a burn-rate signal, and a decent answer to "is this subscription worth it".
Prices come only from vendors' published tables and are dated, so a rate change
applies from the day it took effect. A model we don't have a published price
for shows its token count at $0 and is named in the menu. Prices are never
guessed — if you see an unpriced model, please
open an issue with a link to the
published rate.
Priced today (Anthropic's table re-verified 2026-09-09, with Opus 5.5 and
Fable 5.1 read 2026-09-23; OpenAI's re-read 2026-09-25): Anthropic's Fable 5, Fable 5.1, Mythos 5, Opus 5, Opus 5.5, Opus 4.8, Sonnet 5,
Sonnet 4.6 and Haiku 4.5; OpenAI's gpt-6-astra (the current Codex flagship),
gpt-6-sol, gpt-6-luna, gpt-5.6-sol / -terra / -luna, gpt-5.5,
gpt-5.4 and gpt-5.4-mini, with cache writes at OpenAI's published "Cache
writes" rate where it prints one. codex-auto-review has no published rate
and stays at $0 by design; any other unpriced model with heavy use in a day
notifies you once.
rollups.json only keeps lookback_days (30 by default), and Claude Code
prunes ~/.claude/projects on its own schedule, so once a day ages out there is
nothing left to rebuild it from.
The widget therefore keeps a second copy in history.sqlite, in the install
directory, 0600. It is a mirror of the same per-day, per-model token counters
— no prompt, path or session text, the same as everything else the widget writes
— and it is append-only: days are added and corrected, never removed. A
rebuild or a repair that lowers a figure is copied across, so the long-term
record cannot preserve a number the live store has since fixed.
Under Cost in the menu, once the mirror has actually recorded something:
- Export usage (CSV)…
- Export usage (JSON)…
Both write ~/Downloads/usage-bar-<today>.csv / .json and reveal the file in
Finder. Before the first scan has finished — and on a machine where history is
off — the two items are replaced by a single ! history: nothing recorded yet
line, because a file with a header and no rows is not an empty answer: read back
a week later it is the claim that nothing was spent. Each row is one day × one
model:
| Column | Meaning |
|---|---|
day, vendor, model |
the cell |
input, output, cache_read, cache_write_5m, cache_write_1h, total_tokens |
the counters |
usd_at_record_notional |
what that day cost at the rates in effect on that day |
usd_at_today_rates_notional |
the same tokens repriced at today's table |
Two dollar columns, because a price change should read as a difference rather than quietly rewrite your past. Both are notional.
To switch the mirror off, set "history_enabled": false in settings.json; the
file is then never opened or created, and the Cost section goes back to exactly
what it drew before this feature existed — no export items, and no line about a
mirror you deliberately turned off.
Cost → Open dashboard writes one HTML file to the install directory
(dashboard.html, 0600) and opens it in your browser. It is regenerated on
every open, so it is a snapshot of what the stores held at that moment, with the
timestamp printed at the top.
It answers the questions a menu cannot: how the last 30 and 90 days compare, how tokens and notional dollars split by vendor and by model, how each vendor's model mix moved day by day (this is where "Astra took over from Sol" is visible), which windows each account is currently sitting in, and how much volume is unpriced.
Workflow swarms count toward the session that ran them. When one ran today, the
menu's Cost section adds today's top workflow runs (wf_<id> · <project>,
tokens, notional $; behind Settings ▸ Cost by project), and the dashboard's
Workflow runs table splits today's and yesterday's runs by phase, model and
dearest agent. Phase and agent names are read from the run's own
journal.jsonl and never stored.
Everything about the file is deliberate:
- It loads nothing. Inline CSS, inline SVG, no
<script>, no CDN, no network of any kind. It is a record of your spend, so it does not announce itself to anyone, and it will still render years from now with no library available. - Light and dark, following the browser via
prefers-color-scheme. - Every number is a measurement. Compact figures (
1.2M) sit beside their exact integers so the page reconciles against the menu, unpriced models are named with their token magnitude and never given a dollar amount, and an empty store renders an honest empty state rather than sample data. - Days inside the 30-day window come from the live store; older days come from
history.sqlite. Where both hold a day the live store wins, because a rebuild or an audit repair lands there first. - What is not measured is not drawn. Nothing in the widget stores a quota time series, so the "hours at the wall" panel reports the wall each account is at right now and says why a weekly total is not derivable, instead of estimating one.
To switch it off, set "dashboard_enabled": false in settings.json: the menu
item disappears and no dashboard.html is ever written or opened.
Settings ▸ Rebuild cost index re-reads every transcript from the start. It is the right move after a corrupt scan state or a suspicious total — but it is not free: a day whose transcripts Claude Code or Codex has since pruned cannot come back, so a rebuild can legitimately produce less than it destroyed.
So it snapshots first. Before anything is cleared, rollups.json, the scan
states and the dedup sidecar are copied into backup-state-<timestamp>/ in the
install directory, and the confirmation dialog names that directory before you
agree to anything. If the copy cannot be written, the rebuild does not run.
Settings ▸ Restore last backup… puts them back. Indexing then stops until you relaunch the widget: a scanner reads its offsets once, so a running process holds the post-rebuild offsets while the files on disk are the pre-rebuild ones — scanning on would double-count and saving would undo the restore. The menu says so in the diagnostics block, the restored figures are shown, and a relaunch picks everything up normally. Backups are never deleted by the widget.
No icon appeared in my menu bar
The most common first-run problem, and usually not a crash — check
ps aux | grep cc_usage_widget first.
macOS assigns menu bar slots by a stored per-app position. On a full menu bar (very likely on a notched MacBook), an item with no stored position silently loses arbitration and is never drawn — no error anywhere. The widget seeds its own position to avoid this, but if the bar is genuinely full something has to give.
The full title grows with live alerts, so an item that fitted yesterday can
lose its slot after a reboot. The widget notices and falls back to the compact
title on its own within about a minute (title: status item hidden by a full menu bar in logs/widget.log); if even that does not fit:
Fixes, in order:
- Quit a menu bar app you don't need and restart the widget.
- Shrink the widget: Settings → title — turning the cost text off leaves an icon-only item roughly a third of the width.
- Confirm it is otherwise healthy:
./run.sh --dry-runprints the composed menu and every path it uses, without touching the menu bar.
The accounts section says claude-swap is not installed
Expected — account bars, switching and auto-switch come from claude-swap. Cost tracking and the Codex section work fine without it.
Cost says "indexing…"
The first scan is still running. It shows indexing… rather than a partial
number that would look like a real total. Large corpora take under a minute.
install.sh says Python 3.12+ is required
macOS ships Python 3.9, which cannot run this. Install
uv (it brings its own Python) and re-run
./install.sh.
python tests/test_cost_math.py # Claude cost math
python tests/test_codex.py # Codex extraction
python tests/test_privacy.py # the canary: no transcript content escapes
python tests/test_regressions.py # everything previously broken
python tests/test_statebackup.py # the backup taken before a rebuildEach file is its own runner (there is no test framework to install) and exits
non-zero on a failure. .github/workflows/tests.yml runs every
tests/test_*.py on a macOS runner that has neither ~/.claude nor ~/.codex
— which is the point: most of the guarantees below are claims about a machine
with no corpus, and only a machine with no corpus can check them.
The tests are the interesting part of this repository. AI transcript accounting is full of traps that produce a plausible wrong number rather than an error — cumulative counters that reset mid-session, cached tokens that are a subset of input rather than an addition, a model attribution that has to survive a scan resuming mid-file. Each has a test that fails on the naive implementation.
Design notes: docs/SPEC.md, docs/SPEC-CODEX.md.
Account features build on claude-swap by @realiti4 — a separate project, gratefully used.
Not affiliated with, endorsed by, or sponsored by Anthropic or OpenAI. "Claude", "Claude Code", "Codex" and "ChatGPT" are their respective owners' marks, used only to describe what this reads. Pricing is reproduced from public pages and may be out of date — check the vendor's own page before making decisions about money.
MIT licensed.