Claude Code Status
A VS Code status bar item showing what Claude Code is doing and how much of your
usage window you have spent.


How much of your subscription limit you have spent, then the time until it
actually resets. Hover for a breakdown; click for a full panel. Set
claudeStatus.primaryMetric to cost for locally-measured spend instead.
The icon says what Claude is doing. All five are variations on one mark, drawn
by scripts/icons.mjs and shipped as media/claude-status.woff:

The working mark above twinkles at the cadence the status bar actually
plays: four frames, ping-ponged, 200 ms apart.
|
|
|
claude-ask |
Needs your input |
A question, a plan to approve, or a permission dialog. The item is highlighted too — see attentionStyle. |
claude-pulse-* |
Working |
Generating, or running a tool. The mark twinkles while the turn is live — see animateWorking. |
claude-orbit |
Background work |
The turn is over but a subagent, backgrounded command, or async agent is still going. Static on purpose: the main session is not the thing that is moving. |
claude-done |
Done |
The turn just finished and you have not looked yet. |
claude-mark |
Idle |
Nothing happening. |
The working icon animates by swapping frames on a timer rather than with VS
Code's ~spin, which is granted to four built-in codicons only and does nothing
on a contributed one. The timer runs only while a turn is actually live in a
focused window, so an idle or backgrounded editor costs nothing.
The two states that are easiest to miss are the point: Claude blocked on you, and
Claude finished. Both are invisible in a status bar that only knows busy and
idle, and both are cases where a transcript that has simply gone quiet looks
exactly like one that is done.
With several sessions running, the state shown is the one most worth acting on —
needs your input outranks working, which outranks background, done, and
idle. A ×3 badge next to the icon means three of them are in that state; the
tooltip and panel name each session individually, by conversation title.
Those two figures come from different places, and the extension keeps them
visibly apart everywhere — the hover is where you can see the seam:

- Plan limits — your real session and weekly utilization and reset times,
reported by Claude for your whole account, across every device.
- Usage analysis — cost and token totals computed from this machine's
transcripts. The open window uses your real session boundaries when a fresh
reading is available (tagged actual); earlier ones are grouped from local
activity and tagged estimated.
How it works

Claude Code writes a JSONL transcript for every session under
~/.claude/projects/<encoded-cwd>/<session-id>.jsonl. Each assistant message
carries the exact usage object the API returned, so this extension reads those
files directly. No API key is involved anywhere.
The one thing not derivable from transcripts is how much of your subscription
rate limit you have spent. For that the extension refreshes from the same
endpoint Claude Code's /usage uses, reusing the OAuth token Claude Code already
stores. Set claudeStatus.planLimitSource to cache to turn that off; the
extension then reads only Claude Code's local cache and makes no network requests
at all. See Plan limits.
Two details of that format matter, and both are handled:
- One line per content block. A single assistant message is written once per
content block, and every line repeats the message's full
usage. Naively
summing lines roughly doubles the totals; records are keyed on message.id
instead.
- Resumed sessions copy their history. The same
message.id reappears in the
new session's file. Deduplication is global across files, keeping the earliest
timestamp so tokens are attributed to when they were actually spent.
Files are re-read incrementally — only bytes appended since the last scan are
parsed — so the default 10-second refresh stays cheap on large histories. A full
cold scan of ~3,400 messages across 79 sessions takes about 200 ms.
The 5-hour windows are an analysis grouping, not your limit window
Claude's session limit runs on a 5-hour window, so usage is grouped the same way:
a window opens at the top of the hour of its first local message and runs five
hours; a message past that, or after a five-hour gap, opens a new one.
These boundaries are not your plan's boundaries, and the extension no longer
pretends otherwise. Two reasons they drift:
- The server anchors on the exact instant of your first request. Flooring to the
top of the hour moves the start back by up to 59 minutes, and once one boundary
is early every later one is too.
- Grouping only sees this machine. Usage from claude.ai, another device, or
outside
lookbackDays never appears, so the message that opens a local window
often is not the request that opened the real one.
Observed on a live account: the local window read 6:00 PM → 11:00 PM while the
real session window ran 1:19:59 PM → 6:19:59 PM — a countdown wrong by nearly
five hours.

The open window is the exception. The API returns resets_at but no start or
duration field — however the window is exactly five hours (watched one roll over
from 16:20:00 to 21:20:00 UTC), so the current window's start is
resets_at - 5h. When a fresh reading is available the extension uses that,
labels the window actual, and counts only the messages genuinely inside it.
Earlier windows keep the estimate and are labelled estimated.
Earlier boundaries cannot be recovered the same way: windows re-anchor after an
idle gap and the phase drifts with them (:10 past the hour on one day, :20 on
another), so stepping backwards in five-hour steps produces plausible-looking but
wrong boundaries.
Estimated windows therefore show no reset time at all. Reset times appear only
under Plan limits, where they are real, and the status bar countdown is
omitted entirely when no recent reading is available.
Plan limits
Claude Code caches the result of /usage in ~/.claude.json under
cachedUsageUtilization: your session (5-hour) and weekly limits, each with a
utilization percentage and a reset time, plus any model-scoped weekly limit. The
extension reads that block and shows every limit in the tooltip and the details
panel.
That cache is only rewritten when Claude Code actually calls the usage endpoint,
so on its own it can sit unchanged for days. By default the extension refreshes
it — at most once every planRefreshIntervalSeconds (5 minutes), and never while
every session is idle, so an editor left open generates no traffic. It reads the
OAuth access token to make that call and never writes it, or anything else, back
to disk; ~/.claude.json is Claude Code's state and is only ever read.
Readings are handled honestly rather than optimistically:
- A limit whose reset time has passed is reported as reset since last check,
not as a percentage that no longer describes anything.
- A reading older than
planStaleAfterMinutes (60) is still shown, labelled with
its age, but stops driving the status bar figure and its colors.
- If the refresh fails for any reason the extension quietly falls back to the
cached reading. The endpoint is undocumented and may change; a broken refresh
degrades to older numbers, never to an error. It also rate-limits, which is why
refreshes are throttled and skipped while every session is idle.
A real limit's percentage is what the status bar shows by default
(claudeStatus.primaryMetric: planLimit); claudeStatus.planLimitFocus chooses
which one:
planLimitFocus |
Shows |
max (default) |
Whichever limit is closest to being reached. |
session |
Your 5-hour session limit. |
weekly |
Your account-wide weekly limit. |
perModel |
The weekly limit for a specific model, e.g. Opus — the highest of them if your plan has several. |
The reset countdown belongs to the same limit as the percentage, so the two are
always about one window. It stays a single number — every label, reset time and
staleness note lives in the tooltip, which tags the limit the status bar is
quoting — and falls back to cost whenever no fresh reading is available, or when
your plan has no limit of the kind you picked. It never quietly shows a different
limit than the one you asked for.
The amber and red highlight follows the same choice, so the color always agrees
with the figure it is coloring. Worth knowing which way that cuts: with
planLimitFocus: session, a weekly limit at 95% no longer lights up the item,
even though it is the one that will actually stop you. The default max watches
every limit on your account and is the safer setting; the tooltip and details
panel list all of them either way. Your blockCostLimit budget and the
"waiting on you" highlight are unaffected by this setting.
The highlight
The background color has three possible causes, and nothing in the item can say
which one fired, so each has its own switch:
| Cause |
Switch |
Your blockCostLimit / blockTokenLimit budget |
claudeStatus.highlightBudget |
Your subscription rate limits, per planLimitFocus |
claudeStatus.highlightPlanLimits |
| A session waiting on you |
claudeStatus.attentionStyle (color colors, icon and label do not) |
Both highlight switches are color-only: turning one off leaves its figures,
tooltip and panel exactly as they were. That is the difference from
showPlanLimits: false, which removes the plan feature altogether. Icons stay
reserved for what each session is doing — usage never changes the icon.
Waiting-on-you always enters as a warning, so it can never mask a real budget or
plan-limit error underneath it.
Costs are equivalent pay-as-you-go API prices for the tokens recorded,
computed from the published per-model rates (including cache reads at 0.1x input,
5-minute cache writes at 1.25x, 1-hour cache writes at 2x, and fast-mode premium
rates). On a subscription plan no one is billing you these amounts — treat the
number as a consistent measure of consumption. Tokens from models the extension
does not recognise are counted in token totals but contribute nothing to cost.
No API publishes prices, so the rate table is compiled in. To price a model
released after this version — or to correct a rate that has changed — set
claudeStatus.modelRates. Keys are matched as prefixes against the model id, so
one entry covers a model's dated snapshots:
"claudeStatus.modelRates": {
// USD per million tokens.
"claude-opus-6": { "input": 5, "output": 25 },
// Optional: fast mode, when it costs more than the standard rates.
"claude-opus-7": { "input": 5, "output": 25, "fast": { "input": 10, "output": 50 } }
}
Your entries are checked before the built-in table and replace whatever they
match, so a broad "claude-opus" reprices every Opus model; among your own
entries the longest prefix wins. Cache reads and writes follow from the input
rate at the multipliers above.
Settings
| Setting |
Default |
What it does |
claudeStatus.enabled |
true |
Show the status bar item. |
claudeStatus.primaryMetric |
planLimit |
planLimit, cost, tokens, or percent. |
claudeStatus.planLimitFocus |
max |
Which subscription limit planLimit reports, and which limit the countdown belongs to: max, session, weekly, or perModel. |
claudeStatus.modelRates |
{} |
Rates for models the built-in table does not know, or corrections to ones it does. See About the cost figure. |
claudeStatus.blockCostLimit |
0 |
Budget per 5-hour block, in USD. Enables percent and threshold colors. |
claudeStatus.blockTokenLimit |
0 |
Token budget per block. The tighter of the two limits wins. |
claudeStatus.warningThresholdPercent |
75 |
Turn the item amber above this share of the block budget, or of a plan limit. |
claudeStatus.errorThresholdPercent |
90 |
Turn the item red above this share of either. |
claudeStatus.highlightBudget |
true |
Let the block budget color the item's background. Off changes the color only. |
claudeStatus.highlightPlanLimits |
true |
Let your plan limits color the item's background. Off changes the color only. |
claudeStatus.scope |
all |
all counts every session on the machine; workspace counts only sessions inside this workspace. |
claudeStatus.refreshIntervalSeconds |
10 |
How often to rescan. |
claudeStatus.lookbackDays |
30 |
Ignore transcripts older than this. |
claudeStatus.claudeHome |
"" |
Override the Claude directory. Empty auto-detects $CLAUDE_CONFIG_DIR, then ~/.claude. |
claudeStatus.showResetCountdown |
true |
Show time until the limit beside it resets. Needs a recent plan reading. |
claudeStatus.showActivity |
true |
Show what each session is doing. Off pins the icon to the resting mark. |
claudeStatus.animateWorking |
true |
Twinkle the icon while Claude is working. Pauses when the window loses focus, and never runs when workbench.reduceMotion is on. |
claudeStatus.attentionStyle |
color |
How loudly to flag that Claude is waiting on you: icon, color, or label. |
claudeStatus.resultReadyMinutes |
5 |
How long a finished turn reports a ready result. 0 switches the state off. |
claudeStatus.activeSessionTimeoutSeconds |
120 |
How recently a transcript must have changed to count as active. A session Claude Code reports as busy or waiting stays listed regardless. |
claudeStatus.alignment / claudeStatus.priority |
right / 100 |
Placement in the status bar. |
Neither limit is set by default, because Anthropic does not publish subscription
limits in token or dollar terms. Set blockCostLimit to whatever a heavy block
looks like for you (watch the panel for a few days) to get a meaningful
percentage and threshold colors.
Commands
- Claude Status: Show Usage Details — panel with the current block, per-model
breakdown, recent blocks, active sessions, and any background work running.
Sessions are listed by conversation, since directory and branch do not
identify one: parallel conversations in the same repo are common.
- Claude Status: Refresh Now
- Claude Status: Open Session Transcript — opens this workspace's transcript.
Logs
Everything on the plan-limit path fails quietly: the status bar falls back to the
cost figure rather than showing an error, because a status bar item is the wrong
place to report that a refresh did not happen. That leaves a window which has
quietly stopped refreshing looking exactly like one whose numbers have not moved,
so there is a Claude Code Status output channel that says which it is.
Nothing appears at the default level. Raise it with Developer: Set Log Level…
→ Claude Code Status:
- Debug — the events: a request made, a reading adopted from another window,
a rate limit and the wait it imposed.
- Trace — adds a line per tick explaining why no request was made, which is
what to turn on when the answer is "nothing is happening".
One line is logged at the default level, when the window starts: the version, the
refresh cadences, the directory being read, and whether this host gave the
extension storage the other windows can see. It is the first thing worth quoting
in a bug report.
No prompt text, transcript content, account id, or token is ever written to it —
only counts, decisions and durations.
Development
pnpm install
pnpm test # 312 unit tests, no VS Code required
pnpm run preview # print the status bar line against your real ~/.claude
pnpm run smoke # load the built bundle against a stubbed vscode API
pnpm run watch # then press F5 in VS Code to launch the extension host
pnpm run preview is the quickest way to check a parsing change against real
data; it accepts --home <dir>, --scope-to <dir>, and --limit <usd>.
To build a .vsix:
pnpm run build && npx --yes @vscode/vsce package --no-dependencies
Releases are published by .github/workflows/publish.yml: bump version in
package.json, commit, and publish a GitHub release tagged v<version>. One
.vsix is built, checked, and sent to the Marketplace. Running the workflow by
hand rehearses all of that without publishing.
Layout
| File |
Role |
src/scanner.ts |
Incremental JSONL reader and deduplication. |
src/aggregate.ts |
Pure functions: 5-hour blocks, time windows, burn rate. |
src/pricing.ts |
Per-model rates and cost computation. |
src/limits.ts |
Plan rate-limit parsing, staleness, severity, plan naming. |
src/planSource.ts |
Reads Claude Code's cached /usage block from disk. |
src/usageClient.ts |
Optional live refresh of plan limits, its throttle and backoff. |
src/planStore.ts |
The refresh record every open window shares. |
src/planRefresh.ts |
Assembles a plan reading and decides when to fetch one. |
src/log.ts |
The output channel's seam, and the lines describing a window. |
src/view.ts |
Status bar text, tooltip, severity. |
src/panel.ts |
Details webview. |
src/extension.ts |
Activation, settings, refresh loop. |
aggregate, pricing, format, limits, planSource, usageClient,
planStore, planRefresh, log, and view have no vscode import, which
is why they are directly unit-testable.
Every VS Code window runs its own copy of the extension. Reading transcripts
that many times over is cheap and harmless, but the one network call is neither,
so the windows coordinate through a small record in the extension's global
storage: whichever window is due makes the request, and the rest read its answer
instead of making their own. If the endpoint asks for a pause, every window
takes it.
Limitations
- Usage is derived from local transcripts, so it reflects sessions on this
machine only — not claude.ai, other devices, or teammates.
- Rate-limit utilization does not come from transcripts — they carry no
rate-limit fields at all. It comes from Claude Code's
/usage cache, refreshed
over the network unless you turn that off, and is therefore a point-in-time
reading rather than something recomputed from local data. With
planLimitSource: cache the figures are only as fresh as your last /usage.
- The
blockCostLimit / blockTokenLimit percentages remain measured against a
budget you configure, which is unrelated to your real plan limits.
- Session state comes from two places. Claude Code records what each of its
running processes is doing in
~/.claude/sessions/<pid>.json, which is the
only way to see that it is blocked on a permission dialog — that leaves no
trace in the transcript. Everything else is reconstructed from the transcript
itself. The two are reconciled on newest-evidence-wins, because the status file
describes only the main thread and can lag it by minutes.
- Both sources can be wrong for a while. A session killed mid-turn writes no
closing record, so it reads as working for ten minutes before timing out; a
backgrounded command with no completion notification is written off after six
hours. "Needs your input" deliberately never times out, but stops reaching the
status bar after eight quiet hours.
- Model rates are compiled in. A model released after this version ships shows
tokens but no cost until the table is updated, or until you price it yourself
with
claudeStatus.modelRates.