Repository navigation
Releases
v0.6.8
Fixed
- Other icons were drawn over the menu bar title. 0.6.7 dropped to the glyph
alone whenever macOS hid the item, and restored the title on every app switch.
macOS widened the item to the right, over icons it had already packed into the
space the glyph had freed, then pushed it back under the notch — the app's own
log shows it flipping every few seconds, and a screenshot shows two other icons
across "100% ~$302". The automatic shrinking is gone; hidden spells are still
logged and reported bydoctor. - The title was re-applied every two seconds even when unchanged, each time an
invitation to lay the menu bar out again. It is now set only when it changes. - The item's width changes as rarely as the text allows. It grows at once,
since the text must fit, and shrinks only once a narrower title has lasted ten
minutes, so a percentage falling at a window reset does not reshuffle the bar.
Checked on a real menu bar with the new--probe-width(84 → 118 → held at 118;
the old behaviour sprang back to 84), which also runs in CI.
v0.6.7
Fixed
-
The menu bar icon kept disappearing. On a MacBook, macOS places menu bar
icons that do not fit under the notch, where nothing is drawn — the app keeps
running and its window still reports itself visible, so it looks like a crash.
The room changes with the frontmost app's menus and with which display's menu
bar is active, so the icon came and went. The app now checks where it actually
is on every poll (under or left of the notch, or past a display's edge), drops to
the glyph alone when the full title does not fit, and tries the full title again
when you switch apps or displays. Verified on a real menu bar: an over-wide title
placed 415pt past the display edge was detected, and the 34pt glyph that
replaced it was back on screen. -
A position you dragged the icon to was forgotten on every launch. The status
item had no autosave name, so macOS returned it to the leftmost spot — the first
one hidden. ⌘-dragging it toward Control Center now sticks. -
--probe-menubarchanged your menu bar style and left it changed. It wrote
each style to the preference to measure it; it now restores yours. -
The smoke test could pass a real credential leak. Its privacy checks piped
stringsintogrep -qunderpipefail; grep exiting at its first match made
the pipeline fail with SIGPIPE, which theifread as "nothing found". Shown
with a key-shaped string at the start of a 4 MB file: reported clean. Every
script now captures, then matches, and a test rejects the pattern anywhere.
Added
-
scripts/hook-e2e.sh, now in the repository and in CI: the status line hook
installed into a real settings file, run through the exact command Claude Code
runs, then uninstalled; subagent and local-model accounting; and the snapshot
the menu bar reads. It refuses to report a pass unless every check ran. -
doctorsays when macOS has been hiding the icon, from a short log the app
keeps of the transitions (positions and display widths only), and how to stop it. -
--probe-fitforces an over-wide title on the real menu bar and reports
whether the app detected it and whether the glyph then fit. -
doctorcalled a stopped source "ready". The Claude desktop app changed in
October 2026: it now fetches your usage at launch and keeps polling only if its
menu bar icon was opened within a server-set number of hours. On the machine
this was found on, its record stopped at the app's launch and stayed silent for
six days, whiledoctorreported it ready and the Claude percentage froze.
doctornow shows silent since … for a plan-usage file with no sample in
two hours, and says how to resume it.
v0.6.6
Fixed
- "Today" began at UTC midnight, so the menu bar could show nothing. Every
day boundary — today, yesterday, this month, the daily report and the hourly
chart — was cut in UTC. In India that put midnight at 05:30: usage at 01:21 was
filed under yesterday, and at 13:30 the menu bar showed—for a day with real
usage. The hourly chart's bars were labelled in UTC too, five and a half hours
off. All of them now follow your own time zone, including on clock-change days. - A few cents of spend showed as
$0.0. The compact menu bar title rounded to
one decimal, so a light day read as nothing spent. Amounts under a dollar keep
their cents, and anything above zero that would still round to zero shows as
<$0.01. - The menu bar app could lose its engine on
brew upgrade. The app runs the
engine with a GUI app's minimal PATH, so the engine recorded its own Homebrew
Cellar path — one naming a versionbrew upgradedeletes. It now records the
stable/opt/homebrew/binpath, as it already did from a terminal.
Added
- Tests that run the engine in other time zones, and CI runs the whole suite
under Asia/Kolkata and Pacific/Kiritimati as well as UTC — the one zone where
this bug could not show. scripts/menubar-e2e.sh: a transcript on disk, a real scan and the title
the compiled app computes, in four zones and both title styles. It runs in CI
against every build, and fails on 0.6.5. The smoke test now also checks that
"today" starts at the machine's own midnight.
Changed
- The README leads with the install that gives you the menu bar app. It now
opens with./install.sh— app, command line, background scanning and login
item in one command — and putspipx install burn-o-metersecond, labelled as
the command line only. A reader who took the one-liner and waited for a menu bar
icon that could never appear was the likeliest first experience.
Fixed
install.shtold you to look for 🔥 in the menu bar. The status item has
shown the meter-and-flame glyph since 0.6.2, so the one thing it said to look for
was not there.
v0.6.5
Added
- On PyPI.
pipx install burn-o-meter(oruv tool install burn-o-meter) is
now the whole install for the CLI, needing neither git nor a compiler. Published
through Trusted Publishing — PyPI trusts this repository's workflow directly, so
no upload token exists — and the files on PyPI are the ones attached to the
GitHub release, not a separate build. PyPI gets a short project page of its own,
because the README's relative links and images all break there.
Fixed
statusline installsaid it took effect only in new Claude Code sessions.
Running sessions pick it up on their next turn, as observed twice while
releasing 0.6.4; the message now says so.
v0.6.4
Upgrading: brew upgrade burn-o-meter (or reinstall with pipx/uv), then run burn-o-meter statusline install once to get Claude's rate-limit percentage live from Claude Code. Existing data is corrected automatically on the first scan: subagent usage is added, and Codex sessions on a local model are relabelled not metered.
Added
- Claude's rate-limit percentage, live from Claude Code. Claude Code receives
the service's own 5-hour and weekly figures on every response and passes them to
its status line command;burn-o-meter statusline installmakes that command
burn-o-meter, which keeps the two percentages and their reset times, discards the
rest of the payload (working directory, transcript path, session id), and prints
5h 43% · 7d 19%as the status line. An existing status line is kept and still
shown;uninstallrestores it, and so does./uninstall.sh, so removing
burn-o-meter never leaves Claude Code calling a deleted binary. Opt-in:
./install.shsuggests it rather than editing Claude Code's settings for you.
The reset time is the service's own rather than derived. Found because the menu bar read 0% on a window Claude Code had taken
to 43%: the desktop app's last sample predated the window, and it wrote no
other for over half an hour. todayshows Claude's percentage, with its age and reset time, alongside
Codex's. It had shown only Codex's, and told Claude users no quota was stored.
Fixed
- Subagent usage was never counted. Claude Code writes a subagent's turns to
<session>/subagents/agent-<id>.jsonl, not to the session's transcript — none of
their messages appear in the parent file — and the adapter read only the latter.
Found by comparing stored totals against Claude Code's own per-session figures.
The pattern names the transcript exactly, so the.meta.json, tool output and
notes beside it stay unread. - …and a subagent message kept its first, partial copy. Unlike a session's
transcript, a subagent's is written while a message streams, so the first copy
carries a fraction of the output: across real sessions, 49 output tokens in
first copies against 17,686 in the finished ones. The copy with the most output
now wins, within a scan and across scans, and a later copy with less never
lowers a stored figure. - Codex pointed at a local model was reported as unpriced. Codex records who
served a session (model_provider) and the adapter did not read it, so a model
on Ollama or LM Studio said "rate unknown" where the truth is "no rate exists".
It is now not metered, like the same model through OpenCode or Kimi Code.
Verified by running Codex 0.156 against Ollama; sessions already scanned are
re-read once on upgrade and corrected in place. - Any unrecognised option started a second copy of the app. The app treated
a flag it did not know as no flag at all, soburn-o-meter.app/…/burn-o-meter --helpput a second meter in the menu bar and never returned.--helpnow
lists the options, and an unknown one exits with an error. - A reset time was claimed for a window nobody had opened. With the five-hour
figure at 0%, the gap between two idle desktop samples was taken for first use,
and a reset five hours after it was reported for a window that did not exist.
A 0% reading now carries no reset time. python -m burnometerdid not run, though the engine records it as the
fallback way to invoke itself when noburnometerexecutable is on the PATH.
v0.6.3 — a warning before the limit
The menu bar warns before a limit runs out
The rate-limit reading in the menu bar turns red at 90% — the same colour, at the same
threshold, as its row in the popover, now read from one constant so the two can never
disagree. Only the percentage changes colour, never the spend beside it.
It is on by default: a colour needs no permission and cannot interrupt, so there is
nothing to opt into. Menu Bar Shows → Colour the Limit at 90% turns it off.
90% was measured, not chosen. Against a month of real plan-usage history it flagged
every window that went on to run out, with the fewest false alarms of the rules tried —
including a cleverer one projecting whether a window would run out before its reset,
which did no better. No rule can warn more than about fifteen minutes ahead, because
Claude records usage only that often. The full reasoning is in
docs/design-budgets-and-alerts.md.
Claude Opus 5.5 is priced
It arrived unpriced, so its usage showed as —. It is now priced exactly as Anthropic
publishes it, as is Claude Fable 5.1. All twelve current Claude models match Anthropic's
pricing page exactly, including cache reads, which are most of a Claude Code bill and
which nothing previously checked. Opus 5.5 charges 0.05× input for cache reads rather
than the usual 0.1×; that was confirmed against Anthropic's own page before it shipped.
Three fixes make sure the new rates actually arrive:
- An upgrade now reaches people who have run
pricing refresh. A refreshed pricing
file used to win simply by existing, so an older one kept shadowing the newer rates an
upgrade shipped. The more recently generated snapshot now wins. - Retired models keep their rates. Moonshot retired its K2 generation. A model
leaving the catalogue does not change what its tokens cost while it was sold, so a
refresh now keeps those rates instead of retroactively blanking past costs. - A refresh can never price less than the install it replaces.
Also
macos/make-app.shrecognises the failure you get building on macOS 27 with only the
Command Line Tools — SwiftUI's macros ship only with Xcode — and prints the two
commands that fix it, instead of a wall of compiler errors.- CodeQL moves to v4 and
download-artifactto v8, both pinned to verified commit SHAs. - Two CodeQL notes cleared.
Full notes in CHANGELOG.md.
Upgrading
brew update && brew upgrade devopsinside/burn-o-meter/burn-o-meterThe menu bar colour is in the app bundle, not the CLI, so rebuild and reinstall the app:
git pull && macos/make-app.sh --install && open /Applications/burn-o-meter.appOn macOS 27 that needs full Xcode selected. The bundle is ad-hoc signed and still needs
Developer ID notarisation for distribution.
v0.6.2 — the meter in the menu bar
The menu bar carries the meter, not a flame emoji
The status item now shows the same meter-and-flame as the app icon and the org
avatar, as a template image — so macOS tints it for the bar it lands in and inverts
it while the item is held down.
Getting it to sit level took four attempts and a reader's screenshots, because the
reference was wrong: the glyph was being aligned to its own adjacent text rather
than to the bar. Apple's SF Symbols settle it — gauge.medium, speedometer and
flame.fill all centre their ink at 0.498–0.500 and fill ~0.90 of their box. The
image is now built from the shape: the drawing is rasterised, its ink measured,
and the image sized and placed so that ink fills it and is centred, which is what
every other item in the bar does. The title then gets a one-point baseline offset,
because digits and a percent sign have no descender and otherwise ride high.
--check-layout measures Apple's symbols as well as ours, so the target fails
loudly if their convention moves rather than quietly going stale.
Two pricing bugs, one of them live
A refresh that returned nothing was saved. A refreshed snapshot shadows the
packaged one completely, so a file written with zero models left a real machine
unable to price anything — the model carrying 97% of that user's spend rendered as
an em dash, with no error and no symptom but a cost that had stopped existing.
Upstream was healthy when checked, so the bad response was transient and we
persisted it. refresh_snapshot now refuses a result below a plausibility floor
and leaves the existing file alone, and a refreshed snapshot has to earn its
precedence on every read — because refusing to write only helps machines that do
not already have a bad file.
reprice relabelled locally-served events. It made the pricing decision itself
instead of calling the function the scan path calls, so it never learned about
not_metered: one burn-o-meter reprice turned "there is no rate, it ran on your
own hardware" into "we do not know the rate". Invisible, because both render as an
em dash. The decision now lives in one function both callers use.
Supply chain
22 Semgrep findings to none. Every action is pinned to a commit SHA — a tag can be
repointed by its owner, which is how the trivy-action and kics-github-action
compromises reached their users. Dependabot keeps the pins current, now with a
seven-day cooldown. Workflow inputs reach shell scripts through env rather than
${{ }} interpolation, which is substituted before the shell sees the text at all.
The pricing refresh pins both scheme and host: SECURITY.md has always said there is
exactly one egress, and that is now a property of the code, enforced by tests.
Also
- Rate-limit rows show a reading's age whenever the source samples periodically,
not only once it has aged — which is what makes a figure differing from the
Claude app's explicable rather than suspicious. The Claude app asks Anthropic
directly; we read what it writes to disk, about every 15 minutes. - A test asserts the macOS app knows every cost basis the engine can write. It
decodes unknown values asunpriced, which would be a specific and wrong claim. - The
·between percentage and spend is gone; it read as a stray full stop.
Full notes in CHANGELOG.md.
Upgrading
brew update && brew upgrade devopsinside/burn-o-meter/burn-o-meterThe menu bar app is a separate bundle — the glyph and alignment work is in it,
not the CLI:
git pull && macos/make-app.sh --install && open /Applications/burn-o-meter.app--install matters: macOS will not register a login item for an app outside
/Applications, so one built in place never returns after a reboot. The bundle is
ad-hoc signed and still needs Developer ID notarisation for distribution, which
this project does not yet have.
v0.6.1 — the menu bar updates itself
Two menu bar bugs, both reported from real use, both invisible to every check the
project had.
The number only moved when you clicked it
The poll timer fired every two seconds and re-read the payload — but nothing
regenerated it. The only code path that scans ran on launch, on opening the
popover, and from Scan now. The timer assumed the background agent was
keeping that file current, and that agent is optional and off by default, so on
most machines nothing was. The title showed whatever was true when the popover
last opened.
The timer now scans for itself whenever the payload is older than a minute, and
skips it otherwise — which is exactly the case when the agent is running, so the
two never duplicate work. A scan is 15–250ms against a warm database.
A stale reading was presented as current
A quota's age came from the payload's own age_seconds, which the engine computes
once, when it writes the file, and never revises. With nothing rewriting the
file that number stayed frozen — so a reading hours past its 15-minute sampling
interval still classified as current, still led the menu bar, and still had its
percentage rendered as a statement about now. Reported as the percentage sitting
at an old value after the limit had actually been reached.
Age is now derived from observed_at against the clock, so it grows, and a
reading that ages out is drawn back instead of trusted. The "as of X ago" labels
and the stale banner read the same frozen number and are fixed by the same change.
The source was never at fault: Claude's own history does reach 100, and 100%
renders correctly in every menu bar style.
Guarding both
--check-freshness joins --check-layout as a CI self-check on the app binary.
It pins the freshness boundaries, asserts a stale reading never reaches the menu
bar and that 100% does, covers a payload with no observed_at so an older engine
still works, and covers a future timestamp so clock skew cannot produce a negative
age. Reintroducing either bug fails it.
Full notes in CHANGELOG.md.
Upgrading
brew update && brew upgrade devopsinside/burn-o-meter/burn-o-meterIf you run the menu bar app, rebuild and reinstall it — the fix is in the app
bundle, not the CLI:
git pull && macos/make-app.sh --install && open /Applications/burn-o-meter.app--install matters: macOS will not register a login item for an app outside
/Applications, so one built in place never comes back after a reboot. The bundle is
ad-hoc signed and still needs Developer ID notarisation for distribution, which
this project does not yet have.
v0.6.0 — Kimi Code
Kimi Code is now measured, alongside Claude Code, Codex CLI and OpenCode.
Read from ~/.kimi-code/sessions/*/*/agents/*/wire.jsonl. Like OpenCode it routes
to any provider, so it covers Moonshot's own models and anything you run locally.
What reading the real logs turned up
- Only
usage.recordis billable. Every turn also writes twotoken_counting
records carrying atokensfield that looks like usage and is not — across 6/6
real turns it equalsinputOther + outputexactly. Summing them would have
roughly doubled every figure. That identity now ships as the adapter's integrity
check. - Usage is per turn, not cumulative. A three-turn session recorded outputs of
276, 239 and 202 — falling, so not a running total. Checked because Codex looked
identical and was cumulative. - Ollama truncates a prompt larger than
num_ctxand reports the tokens it
actually processed. 8,000 tokens against the default 4,096-token window are
reported as 2,050; the same prompt atnum_ctx=16384reports 8,011. The figure
is honest — it is what the model read — and Kimi records it faithfully. It just
is not the size of what you typed.
Pricing
The packaged snapshot now carries 287 models from 16 vendors, up from 132 from
6. DEFAULT_VENDORS had been extended with Moonshot, Zhipu, Alibaba, MiniMax and
the inference hosts, but the file was never regenerated — so a fresh install could
not price Kimi, GLM, Qwen or MiniMax at all, while the docs said it could. Three
tests now hold the file to the vendor list, to pricing one model per supported
agent, and to the count printed in the docs.
deepseek-chat and deepseek-reasoner no longer carry a rate: models.dev dropped
both in favour of versioned names, and no stale figure was carried forward. Set
your own in ~/.burn-o-meter/pricing.toml if you need them.
Fixed
billing.<provider>now works for every agent, not just the two named as
dataclass fields —doctorwas printing advice to set a key the loader ignored.- A
config.tomlomitting any key could not be loaded at all, because the
defaults were read offslots=Truedataclasses, where the class attribute is a
slot descriptor rather than the default value. - The network-egress table no longer lets a narrow terminal elide the one URL it
exists to disclose. - Report tests no longer depend on the width of the terminal running them.
Full notes in CHANGELOG.md.
Install
brew tap devopsinside/burn-o-meter https://github.com/devopsinside/burn-o-meter
brew install devopsinside/burn-o-meter/burn-o-meterThe macOS app bundle is ad-hoc signed: it runs on the machine that builds it, and
distribution needs Developer ID notarisation, which this project does not yet have.
v0.5.0 — local models, told apart from unknown ones
Upgrading migrates your database on first open. Nothing is lost; see below.
Local models are now not_metered, not unpriced
A model running on your own hardware was reported the same way as a hosted model whose rate we simply do not have. Both show no dollar figure, but the claims differ: one says we are ignorant, the other there is nothing to know. doctor and the scan summary now count them apart.
not metered 1 (qwen3:0.6b) — no per-token rate exists
unpriced 1 (hy3-free)
Schema version 2, and the project's first migration
usage_events gains upstream_provider: a router like OpenCode reports OpenAI, DeepSeek and a local model through one adapter, and only this distinguishes them.
CREATE TABLE IF NOT EXISTS never alters an existing table, so without a migration an upgrade would keep the old shape and fail on the first insert. It runs on open, is keyed on user_version, and is idempotent.
Existing OpenCode rows are dropped so the next scan rebuilds them with the field set — they cannot be repaired in place, because inserts are DO NOTHING by design. Nothing is lost: every row derives from logs your agents still hold. Scan offsets are deliberately untouched.
Verified on a real 2,349-event database: version 1 → 2, column added, offsets kept, stable across repeated opens.
Fixed
- The OpenCode test fixture was never committed.
.gitignorecarries*.dbto protect your real database and caught the fixture too, so those tests passed locally and errored on every CI run. A test now asserts the fixture is tracked. - Six tests decayed with the calendar. The Claude plan-usage fixture used absolute timestamps while the adapter drops samples older than seven days — so they were always going to expire, and did, exactly a week later. Nothing in the product was wrong.
Install
git clone https://github.com/devopsinside/burn-o-meter
cd burn-o-meter && ./install.shFull history in CHANGELOG.md.