Skip to content
| Marketplace
Sign in
Visual Studio Code>Visualization>Claude Usage MeterNew to Visual Studio Code? Get it now.
Claude Usage Meter

Claude Usage Meter

SKKokkula

|
42 installs
| (0) | Free
Real Claude usage in the status bar: session, weekly and per-model windows read from Anthropic's API. A stale reading shows its age instead of posing as current, and every window on the machine shares one request. Unofficial.
Installation
Launch VS Code Quick Open (Ctrl+P), paste the following command, and press enter.
Copied to clipboard
More Info

Claude Usage Meter

Your real Claude usage in the status bar — and when the number stops being current, it says so instead of pretending.

⟳ 21% (resets in 2h 11m) · 24% (resets in 4d 4h) · Fable 12%

The rolling 5-hour session window, then the rolling 7-day window, each with the time until it resets — then the Fable 5 sub-cap, by name. Every percentage is a figure Anthropic reported for your account, and every countdown is that response's own resets_at counted down against the clock.

The bar is deliberately spare. Position says which of the first two figures is which — session first, then weekly — so their labels stay off; Fable keeps its name because no position could identify it, and it is the server's own name rather than one invented here. Every figure is drawn in the colour the theme nominates for its own status bar — chosen by the only party that has seen both the text colour and the background it lands on; see Telling the figures apart for the release that learned this the hard way. A figure that reaches your warning threshold takes the amber highlight and a $(warning) icon, and gets its label back for as long as it holds it, because an alarm has to say which window it is about. The hover names every figure in full, with the sub-cap explanation beside them. If you would rather have the window labels on the bar too, set claudeUsageMeter.compact to false and it renders the labelled form: 5h 21% (resets in 2h 11m) · 7d 24% (resets in 4d 4h) · Fable 12%.

Open VSX Licence: MIT

Unofficial. This is an independent, community-built extension. It is not made by, endorsed by, or affiliated with Anthropic. "Claude" is Anthropic's trademark and is used here only to describe what the extension reads.

What makes this one different

There are a lot of Claude usage extensions, and most of them read the same endpoint this one does. So rather than a feature list, here is what is actually different about it.

  • When a reading goes stale, its age is in the bar, not the tooltip. Throttled, or slept through with the laptop shut, an old reading carries its true age — 12m old — and a $(history) icon instead of the live one. Past 30 minutes it is dropped entirely. A tooltip is not where the claim was made; the claim was made in the bar, to someone who is not hovering.
  • The age is computed against the clock, not at poll time. Close the lid at 40% and open it three hours later and nothing has failed, no error was raised, and a frozen "updated 4m ago" would still be sitting there. Freshness is a property of the render, so it is evaluated at render.
  • One reading per machine — not per window, and not per editor. Every window shares a single request through ~/.claude-usage-meter/, deciding by lock who makes it. Run VS Code and Cursor side by side and they still share one. The 429 backoff is shared too, so a throttle slows the machine down once rather than being rediscovered separately by every window.
  • It never shows a number it did not get. A failed request renders —, not 0%, because a zero looks exactly like a successful reading of an untouched quota. There is no demo mode and no simulated data.
  • The one derived number is off by default and labelled est. The projection is the only figure here that could be wrong without the server having been wrong, so it is opt-in, marked in the bar rather than only the tooltip, and silent whenever it has too little history to be worth stating.
  • It reads a Claude Code OAuth token and nothing else. No browser session cookie, no sign-in of its own, no account credential to paste anywhere. The token is read, never written; sent to exactly one host; never logged, never persisted, never put in an error message — with a unit test asserting no failure reason can echo it.
  • Your terminal too, for free. It renders a Claude Code status line from the snapshot it already holds, for zero extra API requests — and that line is subject to every rule above.

The percentages themselves are read from api.anthropic.com/api/oauth/usage rather than estimated from local transcript files, and per-model sub-caps are shown by the API's own names. Both are worth knowing and neither is unusual; what is above is.

Every claim here is checkable in the source, and the sections below explain the reasoning rather than just the behaviour.

Install: search Claude Usage Meter in the Extensions view, or code --install-extension shivakrishnakokkula.claude-usage-meter. It is on Open VSX too, so Cursor, Windsurf and VSCodium can install it — see Install for what those need, and Which sign-in it reads for the one prerequisite that catches people out.

Where the numbers come from

Anthropic exposes the account's real rate-limit utilisation at GET https://api.anthropic.com/api/oauth/usage, authenticated with the same OAuth token Claude Code already stores locally. The response carries five_hour.utilization, seven_day.utilization and their resets_at timestamps.

The figures are therefore read from the server, not estimated locally — the same numbers the usage page shows. Percentages are passed through in the server's own units with no rescaling, and the reset countdowns count down to those resets_at instants, recomputed against the clock each time the bar redraws. A window the API reports no resets_at for simply gets no countdown.

Every percentage the meter shows is one of these readings. The single exception is the optional projection described below, which is derived, is off by default, and is labelled est. wherever it appears.

An earlier version of this extension computed token counts from transcript files and disclaimed that it could not show a percentage, because the denominator is server-side. That was true of the transcripts, but wrong about the account: the usage endpoint publishes the percentage directly. The transcript path has been replaced.

Fable 5

On Max plans Fable 5 is included, and draws from your weekly limit — you can spend up to half of it on Fable. It also draws down usage faster than other models. So Fable is a sub-cap on the weekly pool, not a separate allowance; a low Fable figure is not headroom that exists independently of the weekly bar.

The Fable figure arrives in the response's limits array — a list of windows, each carrying its own percent, resets_at and, for per-model rows, a scope.model.display_name:

{ "kind": "weekly_scoped", "group": "weekly", "percent": 37,
  "resets_at": "2026-08-05T20:00:00Z", "is_active": true,
  "scope": { "model": { "id": null, "display_name": "Fable" } } }

It is not under a flat seven_day_fable-style key; no response has ever carried one. The extension:

  • reports the window whose display_name names Fable, labelling it with that server-supplied name rather than a hardcoded one, or
  • states its absence if no such window is present — a Fable — segment on the bar and a Weekly Fable — not reported row in the tooltip — never an invented figure.

A model_scoped array — the shape Claude Code's own Account & Usage panel reads — and the flat legacy keys are both still accepted as fallbacks, so an account or server version that sends either keeps working.

It never derives a Fable percentage from an assumed denominator. A number invented from a guessed limit would look exactly like a real reading, which is the one thing this extension will not do.

Other models

Fable is the default because it is the window with a sub-cap worth watching, but it is only the default. claudeUsageMeter.models takes any window the API names — ["Fable", "Opus"], ["*"] for all of them, [] for none — matched case-insensitively and rendered in the order you list them. Until 0.4.0 the Opus and Sonnet windows were parsed and then discarded.

A model segment looks the same on the compact bar as on the labelled one: Fable 12%, named. Compaction strips 5h and 7d because position still says which of those figures is which — except on a figure that has reached your warning threshold, which gets its label back for as long as it holds it. A model window has no position, so its name is the only thing that says what the figure measures, and it stays in every case. (v0.4.15 briefly moved these segments into the tooltip until they warned, which read as the meter losing Fable rather than tidying it — the release after put them back.) Each also keeps its own named Weekly <name> row in the hover.

Names are matched by containment rather than equality, so a server-side rename from Fable to Fable 5 still resolves. That is the same reason the label shown is the server's own and not a hardcoded one: a rename should show through, not silently drop the segment.

A name you ask for that the API does not report renders —, so a window your account genuinely lacks looks different from a typo. ["*"] can only render what arrived, so when nothing does the tooltip says so rather than leaving a gap.

When the API grows a field

A real response carries about a dozen per-model keys that are all null — seven_day_opus, cinder_cove, tangelo and others. Today none of them holds a figure. If one starts to, a parser that reads only what it already knows about would ignore it, and the symptom would be a window your account has and the meter silently omits, with nothing anywhere to suggest looking.

So the meter names what it did not read. Show Details lists any field carrying a real value that this build has no rendering for, and the output channel says so once when it first appears:

Reported but not displayed by this version: (nothing — every figure carrying a value is shown)

Only fields holding an actual number count. Listing the nulls would be a permanent warning about nothing, which is the fastest way to teach someone to ignore a warning. It is not an error either — a response that grows a field is a working response, and the figures on your bar are unaffected. It just stops the growth being invisible.

The one number the server did not give you

Everything above is a percentage Anthropic reported. claudeUsageMeter.showProjection adds the one thing they do not send, and that the percentages cannot answer on their own — am I going to run out?

⟳ 21% (resets in 2h 11m) · 62% (resets in 4d 4h) · est. weekly full in 2d 4h

It is a rate fitted to readings the meter took itself, extrapolated to 100%. So it is the only figure here that could be wrong without the server having been wrong, and it is treated accordingly:

  • it is off unless you turn it on. The claim that every number on your status bar came from Anthropic is worth more than this feature is, and switching it on for everyone would qualify that claim for everyone;
  • it is labelled est. in the bar, not just the tooltip, so it cannot be mistaken for a reading by someone who does not hover;
  • the tooltip states its basis — how many readings, over how long — because a projection from three readings over half an hour and one from forty over a day are different claims, and you are the one who has to weigh them.

It says nothing at all unless there is something to say. No projection appears when there are fewer than 3 readings, when they span under 30 minutes, when the rate is flat or falling, when the window is already full, or when the reading it would hang from is no longer current. With too little history the honest output is no projection, not a vaguer one.

The suppression that matters most: a projection landing after the window resets is not shown. That is the good case — you are not going to run out — and "full in 9d" against a window that resets in 4d is noise wearing the clothes of a warning.

Two things the series handles that a naive rate does not. A reset is a discontinuity, not a negative burn rate: readings from before the last reset are discarded, detected both by resets_at moving and by utilisation falling, since usage does not go down on its own. And gaps are ordinary — VS Code is not always open and laptops sleep, so a fourteen-hour hole in the series is expected and is not itself a reason to refuse.

Absence is rendered as absence

If the token is missing, the request fails, or the response is malformed, the meter shows ⃠ Claude usage — — the slashed circle and the em dash, with the reason in the tooltip. It does not show 0%, because a zero looks like a successful reading of an untouched quota.

That item used to be dimmed as well, and is not any more: the colour it was dimmed to could not be seen on most status bars, so the claim was moved onto the icon and the dash, which render everywhere. See Telling the figures apart, and claudeUsageMeter.absentForeground if you want the dimming back. A failed refresh replaces the previous state rather than leaving a stale percentage on screen dressed as current.

There is no demo mode and no simulated data.

The one exception: a throttled endpoint

HTTP 429 is the single failure treated differently, because it means ask later, not there is no data. Blanking a valid percentage because the server asked us to slow down is the mirror-image of showing stale data as live — both misreport freshness.

So on a 429 the meter keeps the last real reading and states its true age in the status bar itself:

🕘 4% (resets in 3h 2m) · 26% (resets in 5d 1h) · 12m old

The rules that keep this honest:

  • the age comes from the receivedAt stamped when that response arrived, not from the request and not from a guess;
  • it is shown in the bar, not hidden in the tooltip, so it cannot read as current to anyone who does not hover;
  • the live $(pulse) icon is swapped for $(history);
  • past 30 minutes the reading is dropped entirely and the meter returns to —, because by then it says more about when we last got through than about current usage;
  • only a 429 earns this. A network error or an auth failure blanks the meter, since those can mean the machine or the account changed underneath us.

Behind that, the poll backs off exponentially (capped at 30 minutes) and honours the server's Retry-After whenever it asks for longer than our own backoff — never for shorter, or a cooperative server could be talked into a tighter loop than you configured. A manual Refresh Now clears the backoff.

Ageing, when nothing failed at all

A throttle is not the only way a figure stops being current. A suspended machine does not run its timers: shut the lid at 40% and open it three hours later, and the last successful reading is three hours old while nothing in the meter's state says a fetch ever failed.

Until 0.4.0 that rendered with the live icon and no age anywhere, and the tooltip said "Updated 4m ago" — the age computed when the poll ran, frozen on screen ever since. Both were wrong in the direction that matters.

Now the age is recomputed against the clock rather than at poll time, and any reading past twice your refresh interval is shown the same way a throttled one is: $(history), and its true age in the bar. The multiple is derived from your own interval rather than fixed, because "overdue" only means anything relative to how often this install actually polls — missing one poll is ordinary, missing two is not.

Focusing the window also asks for a fresh reading, which is why waking a laptop usually corrects the meter before you have finished reading it. That request is an ordinary one, not a manual refresh: it goes through the same cross-window coordination and the same backoff, so alt-tabbing cannot walk through a throttle the account is meant to be waiting out.

Install

Search for Claude Usage Meter in the Extensions view, or:

code --install-extension shivakrishnakokkula.claude-usage-meter

It is published to Open VSX as well, which is where the forks get their extensions — Cursor, Windsurf and VSCodium among them — since Microsoft's marketplace is restricted to official VS Code builds. The same search finds it there. No per-editor command is listed because the forks name their CLIs differently and several do not document the flag; codium and code-server do, and take the same identifier as above.

Whichever editor you install it in, it reads a Claude Code sign-in and not that editor's own — see Which sign-in it reads.

To build from source instead:

npm ci
npm test
npx @vscode/vsce package --allow-missing-repository --out claude-usage-meter.vsix
code --install-extension claude-usage-meter.vsix

Then reload the window. Or press F5 for an Extension Development Host.

Keeping it up to date

A marketplace install updates itself; nothing below is needed. The rest of this section covers the parallel GitHub Release channel, which exists for machines that install the .vsix directly.

push to main → CI tests → version bump → Marketplace + GitHub Release (.vsix)
                                              ↓
                        scheduled task installs it → reload VS Code

The pipeline. .github/workflows/release.yml runs on every push to main. It tests, resolves the version (bumping the patch number if the current one is already tagged, so nothing needs tagging by hand), packages, tags, attaches the .vsix to a GitHub Release, and publishes to the marketplaces. Everything after the test step is gated on a green suite, so a broken commit never becomes a release.

The GitHub Release needs only the automatic GITHUB_TOKEN. Marketplace publishing needs VSCE_PAT (Visual Studio Marketplace) and OVSX_PAT (Open VSX) as repository secrets; each publish step is skipped when its secret is absent, so a fork without tokens still releases.

The installer. scripts/update.ps1 compares the installed version against the latest release and installs the release artifact when they differ:

./scripts/update.ps1          # install if a newer release exists
./scripts/update.ps1 -Force   # reinstall regardless

It installs the exact artifact CI built and tested rather than rebuilding locally, so no build toolchain is needed on the target machine — only gh (for auth) and code. It exits silently when already current, which makes it safe to run on a schedule.

One residual manual step VS Code imposes: an updated extension only takes effect after the window reloads.

To run it on a schedule:

Run this from the repository root. A scheduled task needs an absolute path, so the path is resolved rather than written out, which keeps the snippet correct on any machine:

$script  = (Resolve-Path .\scripts\update.ps1).Path
$action  = New-ScheduledTaskAction -Execute 'powershell.exe' `
  -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$script`""
$trigger = New-ScheduledTaskTrigger -Daily -At 9am
Register-ScheduledTask -TaskName 'claude-usage-meter-update' -Action $action -Trigger $trigger

Remove it with Unregister-ScheduledTask -TaskName 'claude-usage-meter-update'.

update.ps1 is not shipped inside the VSIX — it is a repository script for bootstrapping a machine that installs releases directly. An extension installed from a marketplace has no use for it.

Preview without VS Code

Renders exactly what the status bar and tooltip would show, and lists the field names the API returned. Your token is never printed.

npm run preview

Settings

Setting Default Meaning
claudeUsageMeter.refreshIntervalSeconds 300 How often to re-query the endpoint (minimum 60), machine-wide — every window shares one request. Short intervals cause HTTP 429, not fresher numbers — see below.
claudeUsageMeter.showSession true Show the 5-hour percentage.
claudeUsageMeter.showWeekly true Show the 7-day percentage.
claudeUsageMeter.showResets true Attach each window's reset countdown to its segment — 23% (resets in 4d 4h). It is the API's own resets_at counted down against the clock. Off saves width; the tooltip shows the countdowns either way.
claudeUsageMeter.compact true Keep the bar to the figures: 2% (resets in 4h 30m) · 31% (resets in 3d 14h) · Fable 49%. Off restores the labelled bar — 5h 2% (…) · 7d 31% (…) · Fable 49%. Model windows keep their name in both modes, and a figure that reaches warnAtPercent gets its label back; the tooltip is identical in both.
claudeUsageMeter.models ["Fable"] Per-model windows to show, by the API's own names. ["*"] for all, [] for none.
claudeUsageMeter.showFable true Deprecated — superseded by models. Still honoured: set to false with no models configured, no model windows are shown.
claudeUsageMeter.showProjection false Estimate when a window will fill at your current rate. The one derived number here — see above.
claudeUsageMeter.warnAtPercent 90 Amber-highlight a figure once it reaches this percentage, compared against the figure as shown, and put a $(warning) icon in front of it. With itemPerFigure on, only that figure is highlighted; with it off the whole meter is. Either way the figure that crossed the line gets its 5h/7d label back on the compact bar, so the alarm says which window it means. The icon is there because the amber is a background, and High Contrast Dark paints none.
claudeUsageMeter.itemPerFigure true Draw the meter as one status bar item per figure — which is what lets the amber land on the one figure that crossed warnAtPercent, and what lets each figure carry a colour of its own if you set one. Off draws a single item, · separators included, with the same text.
claudeUsageMeter.colorSegments true Deprecated — superseded by itemPerFigure. Still honoured: set to false with no itemPerFigure configured, the meter is still drawn as a single item.
claudeUsageMeter.alignment "right" Status bar side.
claudeUsageMeter.priority 100 Status bar ordering. With itemPerFigure on this is the priority of the leftmost figure; the rest follow immediately behind it.

Telling the figures apart

The compact bar is three percentages and two dots — until one of them reaches your warning threshold, which is the one case below. That is the display this extension ships, and for anyone reading it quickly the hard part was never the numbers — it was working out which number was which.

Colour was the obvious second answer, and for several releases it was the answer this extension gave: the session figure cyan, the weekly one magenta, the per-model one green, each with a light-theme variant behind it and the projection muted beneath them all. It does not work. The reason is worth writing down, because the mistake has a very reasonable shape and the fix is a rule rather than a better set of values.

A status bar item is never told what it is drawn on. VS Code lets an extension contribute a colour with defaults keyed on the theme kind — dark, light, highContrast, highContrastLight — and that key is the natural one to reach for, not least because reaching for it feels like having handled the question. It is the wrong key. Theme kind describes the editor's editor: the background behind your code. The status bar has a background of its own, statusBar.background, and no theme is obliged to derive one from the other. Many deliberately do not, because a status bar that reads as a distinct surface is the entire point of styling it. Quiet Light, a light theme by kind, paints its status bar #705697, a mid-dark purple. Worse, the registered default for statusBar.background is #007ACC in both polarities — so every theme that simply declines to declare one, the classic Dark+ and Light+ among them, gets a saturated blue bar.

So the light-theme variants — chosen with care, against a near-white bar, and entirely correct for the bar they were chosen against — resolve exactly as designed on Quiet Light and land at 1.27:1 for the session figure, 1.11:1 for the weekly one and 1.32:1 for the per-model one. The floor for small text is 4.5:1. The weekly figure at 1.11:1 is not hard to read; it is very close to not having been drawn. On the #007ACC bar the Light+ per-model colour measures 1.02:1.

The tempting repair is to pick better hexes, and it fails for the reason above rather than for want of trying. Take the dark-theme variants, which are lighter, and put them on that same purple bar: 3.00:1, 2.18:1 and 2.97:1. All three still fail, and they now fail in a light theme, so no branch of that four-way key contains the answer. There is no set of values that is legible on a background you are never told. The defect was never the values; it was believing there was a value to choose.

Two details made it survive as long as it did. It is invisible from the theme you develop in — every measurement above had to be taken by reading another theme's JSON, because from Dark Modern the feature looks like it works. And on that Quiet Light bar the one figure that was comfortably legible was a Fable window at 100%, which is over the default warnAtPercent of 90 and had therefore already been handed statusBarItem.warningForeground on statusBarItem.warningBackground, discarding its identity colour entirely. The only readable figure on the bar was the one that had stopped using this feature.

The rule

Every foreground this extension chooses for the status bar defaults to statusBar.foreground — the colour the active theme itself nominates for text on its own status bar — chosen by the only party that has seen both the text colour and the background it lands on.

That is deference, and it is deliberately weaker than a guarantee. Inheriting a theme's own choice means inheriting its contrast: Solarized Light pairs #586E75 with #EEE8D5 for its own status bar text, which is 4.39:1, so the meter is now wrong there in exactly the way that theme's branch name and line number already are. The honest claim is never worse than the bar's own labels, and that holds on all nineteen shipped themes. The rule also covers the bar's ordinary background: VS Code repaints the whole bar during a debug session and with no folder open, and an item that names a colour writes it inline and does not follow statusBar.debuggingForeground there.

That is one rule and it covers five things that used to be handled separately: the session, weekly and per-model figures, which carried hexes; the projection, which carried descriptionForeground; and an unreported window's em dash, which carried disabledForeground. It is worth being clear that those last two were not more cautious choices that happened to survive. They were the same choice — a colour picked in the dark. descriptionForeground measures 1.24:1 on that purple bar, which is the muted projection rendered nearly invisible rather than tastefully quiet, and disabledForeground fails the floor on 18 of the 19 shipped themes, reaching 1.02:1. A colour is not conservative because it is grey.

VS Code has already conceded the whole point in its own registration, which is the strongest evidence available that this is not an over-correction: statusBar.foreground defaults to #FFFFFF in light themes as well as dark ones. The editor will not key that colour on theme kind either, because status bar backgrounds are chromatic and dark often enough that doing so would be wrong. An extension knows strictly less than the editor does. It should not be making the bolder assumption.

The one exception stays, and it stays for a stated reason rather than by having been overlooked. A figure at or over warnAtPercent takes statusBarItem.warningForeground on statusBarItem.warningBackground, because that amber is the one background this extension asks for, and so the one background it is entitled to know. A colour chosen against an unknown background is precisely the colour you cannot trust on a known one — nobody picks cyan with amber in mind. VS Code settles it identically and does not consult us: an entry given a warning background is converted into a warning kind on the way across and its own color is dropped in the conversion, so a colour returned there would be a decision invisible in the code and an effect invisible on the bar.

What carries identity instead

Dropping the hues costs a channel, and it is worth being exact about what remains rather than claiming nothing was lost.

Position says which of the first two figures is which — session first, then weekly. It said so before the colours existed, it is why the 5h and 7d labels came off in the first place, and it is unaffected by any of this. A per-model window has no position to lean on, so it keeps its own name on the bar in both compact and labelled modes: Fable 12% is self-describing in a way that 12% is not, the name is the server's own string rather than a hardcoded one, and no theme can wash a word out. A figure that reaches your warning threshold gets its 5h or 7d label back for as long as it holds it, so the one figure carrying an alarm always names its subject in text — a rule that used to be justified by the amber taking the figure's colour away, and which now rests on the plainer ground that an alarm must say what it is about. The alarm itself is in the text too, as a $(warning) icon in front of the figure: the amber is a background, and there is one shipped theme — High Contrast Dark — for which VS Code registers no warning background at all, so the highlight is simply never painted there. A window the API did not report reads 5h — or Fable —, keeping its label for the same reason: 5h — states an absence, where a bare — between two separators merely is one. Every item is named in the status bar's own show/hide menu. And the hover names every figure in full, with the reset countdown, the sub-cap explanation, and the warning written out in words — at or over your 90% warning level — because a claim carried only by a background colour is a claim some readers are never shown.

That accounting is the honest one, and reading it back is what settled the question. Colour was described in this README as saying it "a second time": a redundant channel, laid over channels that already worked. A redundant channel that fails to render costs you a redundant channel. But the thing rendered in that channel here was the figure itself — not a label beside it — so a hue that failed to render did not cost a nicety, it cost the reading. That is the difference between a feature that degrades and a defect.

One loss is real and is not being smuggled past you. An unreported window and the whole-meter "no reading" item used to be dimmed, which said "this is not a figure" on the colour channel as well as in the text. They are not dimmed by default any more, because the colour they were dimmed to was the one that failed on 18 themes out of 19 — a channel that does not render was not reinforcing anything. The claim now rests on the text and the icon: 5h — keeps its label and its hover row reads — not reported; the whole-meter case keeps its slashed circle, and its hover reads No usage data: … above This is not a reading of zero usage. If you want the dimming back, claudeUsageMeter.absentForeground is exactly that knob, and you are in a position to set it safely because you can see your own status bar.

The Claude Code status line that ships with this extension reached the same conclusion first, and by a much shorter route: it puts its colour on the 5h and 7d labels and leaves the percentages in the terminal's default foreground, on the reasoning that a faint 7d is a label you read as text while a faint 41% is a number you cannot read at all. A terminal's background is so obviously unknowable that the temptation never arose. The status bar has now been brought into line with the script it installs, rather than the other way round.

Customising it anyway

The five ids stay declared, and they stay declared because the defaults are now conservative — a default that refuses to guess is only reasonable if there is a supported way to be specific: claudeUsageMeter.sessionForeground, claudeUsageMeter.weeklyForeground, claudeUsageMeter.modelForeground, claudeUsageMeter.projectionForeground and claudeUsageMeter.absentForeground. A theme can restyle any of them, and so can you.

You know the thing the extension does not: which theme you are running, and therefore what colour your status bar actually is. So say it explicitly. Scope the customisation to the theme you checked it against, using workbench.colorCustomizations' per-theme form, so that it cannot follow you into a theme where it would be illegible:

"workbench.colorCustomizations": {
  "[Quiet Light]": {
    "claudeUsageMeter.sessionForeground": "#DDEEFF",
    "claudeUsageMeter.weeklyForeground": "#F8E4F5",
    "claudeUsageMeter.modelForeground": "#D6F2E6"
  }
}

Those three are worked against Quiet Light's actual status bar, #705697, and clear the small-text floor with room to spare — 5.12:1, 5.02:1 and 5.11:1, against a required 4.5:1. They are near-white tints rather than hues, and that is not timidity. On a mid-dark chromatic bar the only axis with real range left is lightness, so a set of colours that are all legible there must all be light, and the difference between them has to be carried by a slight cast over a common brightness: a cool blue on the session figure, a warm pink on the weekly one, a green on the model one. It is a quieter distinction than cyan against magenta. It is also a distinction you can see, which the loud one was not.

The theme scope is the entire lesson of this section, so it is worth stating as an instruction rather than leaving it to be inferred from an example. An unscoped block is a global claim about a background made by somebody who has looked at exactly one — which is the shipped defaults' own mistake, reproduced by hand in your settings file, where nothing will ever review it. Switch to a dark theme with a near-black bar and #F8E4F5 is fine; switch to one with a pale bar and you have written yourself the same 1.1:1 that started all this, and you will have written it deliberately. The scoped form expires when the assumption behind it does, which is the only property that makes a hardcoded colour safe to hold.

If you want to do this for a theme not listed here, the check is short. Find the theme's colour file and read statusBar.background out of it — for the built-in themes that is under the editor's resources/app/extensions/theme-*/themes/ directory — and take the contrast ratio of your candidate against that value, not against the editor background and not against the theme's kind. A theme that declares no statusBar.background at all is inheriting one you cannot see from the file, and the honest answer there is to leave the defaults alone.

Why the bar is drawn as several items

VS Code gives a status bar item exactly one foreground colour and exactly one background colour, and after the change above it is the second of those that shapes the bar. A meter drawn as a single item can raise only one alarm, about all of its figures at once — which is how the amber used to behave, announcing something here is at 95% to a bar that had deliberately dropped the labels that would have told you which. So the meter draws as one item per figure, and since the identity colours are no longer the reason by default, that is the reason that survives for everyone: the highlight lands on the figure that actually crossed warnAtPercent. Per-figure items are still what makes a per-figure colour possible at all, for anyone who sets one.

The cost is the separators. With several items the · between figures is replaced by the status bar's own spacing, which makes for a slightly looser bar than the one this README opens with. That trade is described here rather than left to be discovered, and it is not a close one: a warning that names its own subject is worth two dots.

That split is claudeUsageMeter.itemPerFigure. It used to be half of a setting called claudeUsageMeter.colorSegments, and fusing the two was a smaller version of the same error this section is about: a reader who wanted no identity colours had to pay for that preference with the per-figure warning highlight, which has nothing to do with colour and is much the more important of the two. There is no colour switch any more, and that is deliberate rather than an omission — with every identity colour resolving to statusBar.foreground, a colour switch would do nothing at all for anyone who had not written workbench.colorCustomizations, and for anyone who had it would let this extension override a colour they set by hand. Overrides do not travel in that direction. The off switch for a colour is the colour id itself.

colorSegments is deprecated and still honoured, so nobody's bar changes shape on upgrade: if you set it to false and have not configured itemPerFigure, the meter is still drawn as a single item. Set itemPerFigure and it wins. With the bar drawn as one item the whole meter turns amber together, and there the restored 5h/7d label is the only thing saying which figure raised the alarm — a stronger reason for keeping it rather than a weaker one.

Why the interval is 5 minutes

The endpoint rate limits hard. The original 60-second default predated cross-window sharing, so every open VS Code window ran its own poller — three windows meant three requests a minute, roughly 4,300 a day, against windows measured in 5 hours and 7 days. The precision was imaginary and the cost was a permanently throttled endpoint: during development, 12 consecutive probes over 15 minutes were all refused. The default retreated to 10 minutes.

Two things have changed since. Every window on the machine now shares one request, so the default is a machine-wide cadence rather than a per-window one — 5 minutes is 12 requests an hour no matter how many editors are open. And a day of shared 10-minute polling was measured against the endpoint with zero refusals before the default came down to 5.

Lowering this below a minute is not possible, and lowering it further buys little: the figures are whole percent over 5-hour and 7-day windows, so most of what a tighter poll fetches is the same number again. On a 429 the meter backs off exponentially whatever this is set to.

One reading per machine

Every window shares a single request rather than making its own. Whichever window comes due first takes a lock, asks, and publishes the answer where the others can read it; the rest render that, with its true age, instead of asking again. The backoff is shared too, so a 429 slows the machine down once rather than being rediscovered separately by every window.

Until 0.4.3 that snapshot lived in the extension's own global storage, which is per editor rather than per machine — VS Code resolves it under Code/User, Cursor under Cursor/User. Running two of them side by side therefore produced two independent pollers, which is the exact failure this mechanism exists to prevent, reintroduced by the choice of directory. It now lives in ~/.claude-usage-meter/, so every editor on the machine shares one reading — and so anything else can read it too, which is what makes the status line below free.

Deliberately not inside ~/.claude: that belongs to Claude Code, and a cache of ours has no business sitting among files another program owns.

In your terminal, as a Claude Code status line

scripts/claude-statusline.mjs renders the same figures into Claude Code's status line:

5h 17% (resets in 2h 4m) · 7d 41% (resets in 5d 12h) · Fable 48%

It keeps the 5h/7d labels the status bar no longer shows, deliberately: a terminal line has no hover, so the labels are the only place a figure's name can live there.

Those labels are what carries the colour: 5h cyan, 7d magenta, the model name green, from the sixteen basic ANSI colours your own terminal theme remaps. The percentages, the em dashes and the age are left in the default foreground on purpose, so the colour goes where being washed out on some light profile would cost you a redundant channel rather than the reading itself. A faint 7d is a label you read as text instead of as a colour; a faint 41% is a number you cannot read at all.

That is the same argument the status bar now makes, and this script got there first. Both surfaces refuse to guess at a background they are not told; the difference is only that a terminal's background is so obviously unknowable that the temptation never arose here, while a contributes.colors block keyed on theme kind looked enough like an answer to survive several releases. The status bar was brought into line with this script, rather than the other way round — and the escapes below have not changed, because sixteen colours a terminal theme remaps for itself are not the thing that was wrong.

The age is deliberately not dimmed. It used to be, which was this line quietly doing the thing the status bar refuses to do — making the one annotation that exists to be noticed the faintest text on screen. --plain or NO_COLOR=1 emits no escape sequences at all.

The extension installs it for you. Run Claude Usage Meter: Install Claude Code Status Line from the Command Palette.

It has to be installed rather than run where it sits, because a statusLine command is a literal string in ~/.claude/settings.json and an extension directory carries its version in its name — so a path pointing inside one stops resolving at the next update, silently. Fetching it from the repository is not an option either: this one is private, so the raw URL that would otherwise be the obvious answer serves a 404 to everybody, its owner included.

So the command copies the script next to the shared snapshot, at ~/.claude-usage-meter/claude-statusline.mjs, and prints the settings snippet into its output channel for you to paste — built from the real path, which on Windows is neither ~-relative nor forward-slashed:

// ~/.claude/settings.json
{
  "statusLine": {
    "type": "command",
    "command": "node /home/you/.claude-usage-meter/claude-statusline.mjs"
  }
}

Re-running it is safe, and each extension update refreshes a copy you have already installed, so the script cannot drift from the extension that feeds it. Nothing is written for anyone who has not run the command.

It makes no network request, and that is the design rather than a simplification. Claude Code debounces the status line at 300ms and cancels a script still running when the next update arrives, so a command that called an aggressively rate-limited endpoint would be both killed constantly and the fastest way yet devised to earn your account an HTTP 429. It reads the snapshot the extension already maintains, so showing usage in your terminal costs zero extra requests.

The consequence, stated plainly: if no editor is running, nothing refreshes that snapshot. The line then ages, says so — 5h 17% (resets in 1h 39m) · 7d 41% (resets in 5d 11h) · Fable 48% · 25m old — and past 30 minutes shows claude — rather than presenting a figure from this morning as if it were current. It is subject to every rule the status bar is. The countdowns keep counting through the ageing, because resets_at is an absolute instant that stays true while the percentage beside it does not.

One scoping note: the script reads only the snapshot, so it cannot see your editor settings — claudeUsageMeter.showResets does not reach it, and its countdowns are always on, the same way it assumes the default poll interval when judging freshness.

It is zero-dependency and standalone because a statusLine command has to name a stable path, and an installed extension lives under a directory whose name carries its version and changes on every update. That means it re-implements the honesty rules rather than importing them, so this project's test suite runs the real script and pins each one against the extension's own constants.

Commands

  • Claude Usage Meter: Show Details — full breakdown plus the API field names and the visible network configuration, in an output channel (also what clicking the meter does, on any of the figures it draws)
  • Claude Usage Meter: Refresh Now
  • Claude Usage Meter: Open Official Usage Page
  • Claude Usage Meter: Install Claude Code Status Line — see In your terminal, as a Claude Code status line

Authentication, and what leaves your machine

This extension reads the OAuth token Claude Code has already stored for you. Since that is a credential, here is exactly what it does with it.

Where the token is read from, in order:

  1. CLAUDE_CODE_OAUTH_TOKEN, if set.
  2. ~/.claude/.credentials.json — tried on every platform, macOS included. It is cheaper than spawning a process and cannot raise a system prompt, so an account that has the file is never made to authorise a keychain read it does not need.
  3. On macOS, when that file is absent or unreadable, the login keychain item Claude Code-credentials, read via security find-generic-password. Claude Code generally stores the credentials there rather than on disk, so without this the meter would be blank on every Mac. The first read may raise the system keychain prompt; declining it leaves the meter blank and changes nothing else.

Where it is sent: as a Bearer header to https://api.anthropic.com/api/oauth/usage, and nowhere else. The extension makes no other network request — no telemetry, no analytics, no update check of its own.

What is never done with it: the token is never logged, never written to the output channel, never included in an error message, and never persisted anywhere by this extension. Failure messages name which store failed, never its contents. There is a unit test asserting that no failure reason can echo the credential blob.

The extension only ever reads your credentials; it never writes or refreshes them. If the token expires, re-authenticate in Claude Code.

When the credential store carries an expiresAt that has already passed, the meter says so and skips the request instead of spending a poll on a 401 it can already predict. That check is deliberately timid, because the cost of getting it wrong is asymmetric — a request wasted is one request, a token wrongly judged expired is a meter that never asks again. So it only fires when the value is a plausible timestamp in a plausible range, and only once it is more than five minutes past. Anything unparseable, absent, or out of range is treated as a field we do not understand, and the request is made as normal. The units are read by magnitude, seconds or milliseconds, the same way the usage response's resets_at is: a seconds value misread as milliseconds lands in 1970, which looks exactly like a long-dead token.

Which sign-in it reads

The meter has no credential of its own, and the lookup above is the same code in every editor: nothing in it asks the host what it is, or consults the editor's own account. What it needs is one of those three artifacts present on the machine — in practice, a Claude Code sign-in.

Using Claude through an editor's own AI features does not by itself leave a token where the meter can find it. That matters most in the forks, where the extension is installable but the editor's Claude integration is its own: someone who has only ever signed in to Cursor or Windsurf, and never to Claude Code, has nothing for the meter to read. It reports that as absence, with the reason in the tooltip, rather than as a zero — see Absence is rendered as absence.

CLAUDE_CODE_OAUTH_TOKEN is the exception: it needs no Claude Code install at all. Set it after the editor launched and the editor must be quit and started again from a shell that has it — reloading the window is not enough, because the reload reuses the same main process, and a process cannot acquire an environment it did not start with.

Remote windows, and which machine gets read

In a Remote-SSH, WSL or Dev Container window there are two machines, and ~/.claude/.credentials.json means a different file on each. Which one the meter reads is decided by where VS Code runs the extension host, not by anything in this code.

VS Code runs an extension with an entry point on the remote host unless the extension says otherwise, so until 0.4.0 a remote window looked for a Claude Code sign-in on the server and reported honest absence when it found none — indistinguishable, from the tooltip, from not being signed in at all. The manifest now declares "extensionKind": ["ui", "workspace"], which prefers the local machine: the extension reads a user-level credential and draws a status bar item, and touches nothing in the workspace.

If your Claude Code install is the one on the remote host, that preference is the wrong way round for you. Move it back per-window:

// settings.json
"remote.extensionKind": {
  "shivakrishnakokkula.claude-usage-meter": ["workspace"]
}

Restricted Mode is the same class of problem with a blunter symptom: an extension that declares nothing is disabled entirely in an untrusted folder. The manifest now declares support, because nothing the meter does is derived from workspace content — a fixed URL, a path under your home directory, and on macOS a security call with a fixed argument vector. Opening an untrusted folder no longer takes the meter away with it.

Behind a proxy or a VPN

The one request this extension makes goes through VS Code's own network stack, not around it. Since VS Code 1.96 the extension host replaces globalThis.fetch with a proxy-aware version, so the meter inherits whatever the editor already resolves — including PAC and WPAD scripts, the operating system's proxy settings, proxy authentication, and the OS certificate store. There is nothing to configure here, and no proxy setting of our own to get wrong.

That is also why this extension bundles no HTTP client. Supplying our own would be worse: under the default http.proxySupport: "override" VS Code discards a caller-supplied dispatcher anyway, and the version we could ship would read environment variables only — no PAC, no authentication, no system trust store.

What is left is telling you when the network is the obstacle. A failed request names what actually failed rather than reporting fetch failed:

What happened What the tooltip says
Proxy demanded credentials a proxy on this network requires authentication (HTTP 407)…
Proxy refused the host a proxy on this network refused the connection to api.anthropic.com (HTTP 403)…
TLS intercepted TLS certificate rejected… check http.systemCertificates and that your organisation's root certificate is installed
DNS failure network error: getaddrinfo ENOTFOUND api.anthropic.com [ENOTFOUND]

The last row is deliberately plain. A name that will not resolve is not evidence of a proxy — it is far more often a dropped connection — and saying otherwise would send you to your IT department over wifi.

Two things worth knowing. On VS Code older than 1.96 the extension host does not patch fetch, so no proxy setting can route this request; the failure will still be described, but only upgrading the editor fixes it. And VS Code resolves the effective proxy itself, so a PAC-configured proxy is invisible from inside an extension — an empty http.proxy does not mean no proxy is involved.

Both of those are printed where they are needed rather than left here. Show Details ends with the network configuration this extension can actually see:

Network configuration visible from this extension
  VS Code 1.104.2 — the extension host provides a proxy-aware fetch, so this request follows what the editor resolves from the settings below, PAC and WPAD scripts, the operating system's proxy configuration and the OS certificate store.
  http.proxy: http://***@proxy.corp.example:8080 (set in your settings)
  http.proxySupport: "override" (default)
  http.proxyStrictSSL: true (default)
  http.systemCertificates: true (default)
  http.noProxy: ["localhost", ".corp.example"] (set in your settings)
  http.proxyAuthorization: set (set in your settings) — value withheld, it is a credential
  https_proxy: http://proxy.corp.example:8080 (process environment)
  Caveat: VS Code resolves the effective proxy in its main process, so a proxy that comes from a PAC or WPAD script, or from the operating system's settings, does not appear anywhere above. Nothing set here does not mean no proxy is involved.

The caveat prints every time, including — especially — when every row reads not set. That is the case where the list is most likely to be mistaken for proof that no proxy exists, which is the wrong conclusion in exactly the situation that most needs diagnosing. The heading says visible from this extension for the same reason, and the environment-variable line names the six variables it checked rather than reporting on the category.

That first sentence is assembled from what is actually in force, not from what is usually in force. VS Code's proxy agent resolves a proxy only while http.proxySupport is "override", "fallback" or "on", and loads the OS certificate store only while http.systemCertificates is on — so with either switched off the sentence drops that clause and says which setting did it. Values are printed with their JSON type intact, because "false" is a truthy string that leaves certificate checking on, and a block rendering it as false would confirm the one mistake it exists to catch.

Nothing in that block is a secret. http.proxyAuthorization is a Proxy-Authorization header, so it is reported as set or not set and its value never printed. A proxy URL is reduced to scheme, host and port — never its path or query, which is where a token would sit — with any credentials replaced by ***. A value that cannot be parsed down to a host is withheld whole rather than printed on the assumption it held nothing. And because http.noProxy and NO_PROXY are free-text boxes that people paste proxy URLs into by mistake, each entry in them is checked for credentials too. The view is meant to be pasted into a bug report, and every one of those rules exists so that doing so is safe.

Feedback and reporting a problem

The quickest route is the Send feedback link in the meter's own hover, or Claude Usage Meter: Send Feedback from the command palette. It asks what kind of feedback — a rating, a problem, a suggestion — and opens the right tab of the marketplace listing; for a problem report it opens the details view alongside, so the material worth pasting is already on screen. There is no in-editor text box because the extension has no backend: a form that collected a message and had nowhere to send it would be worse than not asking.

Or go directly: ask in the Q & A tab of the marketplace listing. This repository is private, so there is no issue tracker to open — the Q & A is the channel, and it is read.

Run Claude Usage Meter: Show Details from the command palette first and paste the output. It carries the extension version, the last request's outcome, the age of the current reading and the network configuration described above, which between them answer most of the first round of questions. It is written to be safe to paste; see the paragraph above for exactly what is withheld.

How "it is read" is kept true

Nothing pushes a notification when a question is posted, so that sentence would otherwise rest on someone remembering to look. scripts/check-qna.ps1 polls the tab daily and raises a Windows notification for any question it has not seen before. A check that fails exits non-zero rather than reporting a quiet inbox, and the script reports itself if its last successful check is more than three days old.

This is maintainer tooling — it is not shipped in the VSIX, and it is recorded here because a machine rebuild would otherwise drop it silently while the listing went on making the promise. Registering it, from the repository root:

$script  = (Resolve-Path .\scripts\check-qna.ps1).Path
$action  = New-ScheduledTaskAction -Execute 'powershell.exe' `
  -Argument "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$script`""
$trigger = New-ScheduledTaskTrigger -Daily -At 9:30am
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable
Register-ScheduledTask -TaskName 'claude-usage-meter-qna' -Action $action `
  -Trigger $trigger -Settings $settings

It must run as a logged-on user: the notification is a WinRT toast and needs an interactive desktop session. Remove it with Unregister-ScheduledTask -TaskName 'claude-usage-meter-qna'. Verify the notifier itself with -SelfTest, or the un-notifiable case with -SelfTest -SimulateToastFailure, rather than waiting for a real question to discover it is broken.

Development

npm test        # compile + unit tests, no VS Code and no network required
npm run watch

src/core/usage.ts is pure and offline, so the whole response-schema contract is unit-testable without a token. The vscode dependency is confined to src/vscode/, and network access to src/core/client.ts.

Prior art

Claudemeter is a far more featureful take on the same idea and is where the usage endpoint came from. Use it if you want plan detection, context tracking and per-model breakdowns.

Licence

MIT.

  • Contact us
  • Jobs
  • Privacy
  • Manage cookies
  • Terms of use
  • Trademarks
© 2026 Microsoft