Repository navigation
fix(account-email): show the account the live credentials belong to - #724
Merged
sirmalloc merged 3 commits intoOct 29, 2026
Merged
Conversation
The Claude Account Email widget read oauthAccount.emailAddress from .claude.json. Claude Code writes that record at login and does not rewrite it when the credentials change underneath, so after an account switch the widget could keep naming the previous account, next to usage widgets that read the live credentials and report on the new one. The email now comes from GET /api/oauth/profile, requested with the usage credentials and the usage request's headers, proxy rules and deadline. Answers are cached in ~/.cache/ccstatusline/account-email.json (owner-only) under the usage cache's login fingerprint, so an access-token refresh reuses them and only a login change triggers another request. Up to ten logins are kept, so profiles used side by side do not evict each other. A failed lookup backs off for that login only: 30 seconds, or after a 429 the server's Retry-After. Without an OAuth login, or until a lookup succeeds, the widget falls back to .claude.json as before. Fixes sirmalloc#538
efim0v
force-pushed
the
fix/538-account-email-live-login
branch
from
October 16, 2026 16:22
db5560c to
4618724
Compare
pcvelz
added a commit
to pcvelz/ccstatusline-usage
that referenced
this pull request
Nov 2, 2026
Upstream: weekly limit timers (sirmalloc#768), prompt cache miss widgets (sirmalloc#770), context window size env override (sirmalloc#352, sirmalloc#407), windowed speed fixes (sirmalloc#769, sirmalloc#771), separator/renderer fixes (sirmalloc#773), custom-command styling (sirmalloc#775), clickable cwd links (sirmalloc#744), account email from live credentials (sirmalloc#724), cache timer reported expiry + short toggle (sirmalloc#576, sirmalloc#592), claude-status lock (sirmalloc#635, sirmalloc#772), dedup token metrics (sirmalloc#622, sirmalloc#725, sirmalloc#735). Not adopted: sirmalloc#765 terminal width from COLUMNS with fixed reserve - the fork keeps flexMode/compactThreshold and tmux pane priority. Hand edits outside conflicts: - README.md: kept fork Configure flex separator behavior bullet and older-updates details wrapper; dropped upstream Related Projects additions (claude-eta) - AGENTS.md: kept fork terminal width wording (sirmalloc#765 not adopted) - src/types/FlexMode.ts, src/tui/components/TerminalWidthMenu.tsx, src/tui/components/__tests__/TerminalWidthMenu.test.ts, src/tui/components/TerminalOptionsMenu.tsx, src/tui/components/__tests__/TerminalOptionsMenu.test.ts: restored, fork keeps terminal width settings - src/tui/App.tsx, src/tui/components/index.ts, src/types/index.ts: re-wired terminal width menu and FlexMode export - src/types/StatusJSON.ts: dropped fork duplicate prompt_cache key, upstream schema is a superset - src/utils/terminal.ts: COLUMNS source gated on non-tmux so tmux pane width keeps priority - src/widgets/WeeklyLimitTimer.ts, src/utils/usage-prefetch.ts, src/widgets/__tests__/WeeklyLimitTimer.test.ts: compat fix - fableUsage/fableResetAt -> weeklyFableUsage/weeklyFableResetAt - src/widgets/ApiUsage.tsx: compat fix - Context Bar honors effective context window (override, clamp, 1.0M format); Weekly renders a valid value despite a session error - src/widgets/__tests__/ApiUsage.localModelFallback.test.ts: output tokens no longer counted in context (upstream sirmalloc#730) - src/utils/__tests__/renderer-flex-width.test.ts, src/utils/__tests__/renderer-ansi.test.ts, src/utils/__tests__/gradient.test.ts, src/utils/__tests__/renderer-separator-collapse.test.ts, src/utils/__tests__/config.test.ts, src/tui/__tests__/App.test.ts: fork deviation - fork width model (flexMode, 6-column full reserve, kept settings, Terminal Width menu entry) - src/utils/__tests__/default-colors.test.ts: fork deviation - accept ansi256:/hex: defaults - src/utils/__tests__/usage-prefetch.test.ts: fork deviation - weekly usage comes from the API, fork widget imports from ApiUsage - src/widgets/__tests__/CurrentWorkingDir.test.ts, src/widgets/__tests__/CustomCommand.test.ts: Settings literal gains flexMode/compactThreshold - src/utils/__tests__/context-window.test.ts, src/utils/__tests__/jsonl-metrics.test.ts: restored fork tests upstream dropped - docs/test-retirements.md: ledger for the dev-reload snapshot-path and RemoteControlStatus glyph (sirmalloc#605) tests upstream replaced
eric-engberg
added a commit
to eric-engberg/ccstatusline
that referenced
this pull request
Nov 5, 2026
…and the account key The Claude Account Email widget (sirmalloc#724) and Extra Usage Today each asked /api/oauth/profile about the same login, each with its own cache and backoff. One request now answers both. lookUpProfile runs under the email lookup's per-login lease and backoff files, and each caller keeps its part of the answer in its own cache: the email in account-email-<hash>.json, the account key in usage-account.json, which now holds only named logins. An answer that leaves out the caller's part holds that login off as before, and the other part is still kept. A rate-limited lookup's Retry-After is kept within the day the backoff reader trusts, as the account lookup did, so the email lookup no longer reads a Retry-After further off as no backoff at all.
sirmalloc
added a commit
that referenced
this pull request
Nov 6, 2026
* feat: label Extra Usage as spend on accounts without plan limits
On usage-based plans (Claude Enterprise) the usage API reports every
rate-limit window as null and no limits[] entries: there's no plan limit
to go over, and extra usage is the account's whole spend. The Extra
Usage widgets still called it overage ("Overage Used: $123.45"), and
their picker descriptions only mentioned pay-as-you-go overage, so
Enterprise users had no reason to think these widgets were theirs.
Searching the picker for "enterprise" or "spend" didn't find them.
The parser now flags a response that explicitly reports no plan limits
(`noPlanLimits`). The flag is kept in the usage cache and through the
merge with statusline rate_limits, and the three widgets label their
value "Spend Used", "Spend Left" and "Spend" when it's set. Only an
explicit null counts and any limits[] entry counts as a limit, so every
uncertain case keeps the "Overage" label. A cache written before this
change keeps the old label until its next refresh.
The widget descriptions now mention both Pro/Max overage and Enterprise
spend, which also makes the picker's search find them.
* refactor: share the cache file lock
Move the terminal width cache's best-effort O_EXCL lock into its own
module so other cache files can use the same read-modify-write
protection instead of copying it. No behavior change.
* feat: add a daily state file for per-day usage tracking
Some usage figures are per UTC day, but ccstatusline only ever sees the
current month-to-date totals. Keep a small state file next to the other
caches that records what was seen and when, modeled on the terminal
width cache: one JSON file, the shared lock around each
read-modify-write, an atomic temp-file rename, pruning to today and
yesterday, and injected file and clock dependencies.
* feat: track each day's starting extra usage spend
The usage API reports extra usage spent month to date only. Record the
total per login in the daily state file and work out today's spend from
where it stood when the UTC day began: the last value seen the previous
day, zero on the 1st (extra usage resets at 00:00 UTC), or otherwise the
first value seen today. A total that drops mid-day never makes today's
spend negative, and an observation that loses the state lock to another
process still reports today's spend.
* feat: work out today's extra usage spend during the usage prefetch
fetchUsageData now remembers the token fingerprint of the login it
used. When a line shows today's spend, the prefetch records the
month-to-date total for that login in the daily state file and adds
extraUsageUsedToday (cents) to the usage data. It's worked out on every
render, cache hits included, and never stored in the usage cache.
* feat: add an Extra Usage Today widget
Shows the extra usage spent since 00:00 UTC, including claude.ai and
other machines, labeled like its siblings: "Spend Today" on accounts
without plan limits, "Overage Today" otherwise. It shows nothing until a
usage fetch has run since the UTC day began, and n/a when extra usage
is off, with the same hide options, currency and cost number format as
the other Extra Usage widgets.
* fix: count a cached extra usage total for the day it was fetched
Today's spend recorded every usage prefetch as an observation at render
time, cache hits and stale fallbacks included. A total fetched at 23:58
UTC on the last day of the month and replayed from the cache at 00:01
became the 1st's first observation, so Spend Today showed the whole
previous month for up to three minutes, or for as long as the API stayed
unreachable.
fetchUsageData now reports when its data came from the API
(getUsageFetchedAt: now for a fresh fetch, the cache file's write time
for a cache hit or stale fallback), and the observation is recorded at
that time. A total fetched before midnight updates the previous day's
record, so today's spend shows nothing until a total fetched today
arrives, and an older fetch replayed after a newer one is ignored.
* test: check fetchUsageData reports the login and fetch time
Today's extra usage spend depends on fetchUsageData recording which
login and which fetch its data came from, and every other test replaces
that link with a spy. Run the real fetch in the probe harness and check
it reports the login fingerprint the usage cache is written with and the
API fetch time, and neither when there are no credentials.
* test: cover the Spend label with an edited label
Upstream's label editor (#601) gives each widget one default label,
while Extra Usage Used, Remaining and Utilization pick "Overage" or
"Spend" by account. The editor offers the Overage label, since it has
no usage data, and an edited label replaces whichever one the account
would show.
* refactor: keep the Enterprise wording, drop the Spend label detection
With the label editor (#601) in main, anyone on a usage-based Enterprise
plan can relabel the Extra Usage widgets' "Overage" to "Spend" in a few
keys, so detecting those accounts from the usage API's response shape
isn't worth its cache field and heuristics.
What stays is the wording: the three widgets' descriptions say they
cover Pro/Max overage and Enterprise spend, the docs say the same and
point at the label editor, and picker searches for "enterprise" or
"spend" list the three widgets first, which a new test checks.
* test: check the picker lists the Extra Usage widgets first, however many there are
* test: look for the Extra Usage widgets among the widgets that mention the word
* refactor(widgets): share Extra Usage Used and Remaining's code
The two widgets were the same class apart from how the amount is worked
out, and their label, name, description and preview sample. They now
extend ExtraUsageAmountWidget, the way the Tokens widgets share
TokenCountWidget. Rendering is unchanged.
* fix: drop extra usage times stamped while the clock ran ahead
Extra Usage Today ignores a fetch older than the one it last recorded,
so a cached fetch a concurrent render replays late can't move the day
back. A single render made while the system clock ran ahead defeats
that: it records a fetch stamped at a time that hasn't come yet. Once
the clock is put right, every real fetch looks older and is ignored, so
Today disappears for the rest of the day; and when the clock reaches the
stamped time, that record is taken as the day's, with the previous
day's total as its start. With $10 seen at 09:00, one render a day
ahead, then $25 and $40 later that day, Today was hidden for the rest of
the day and showed $40 the next day for at most $10 of real spend.
The usage cache that render wrote is stamped by the same clock and is
served as fresh until the clock catches up with it, so its fetch keeps
arriving with the future time and would store the same record again.
A record last seen more than a minute ahead of now is now dropped and
the day starts afresh, and a fetch stamped more than a minute ahead
isn't recorded. The minute's leeway keeps a small step back of the
clock, such as an NTP correction just after a fetch, from dropping the
day's starting point. Now is read after the state file, so a fetch
another render has just recorded is never taken for one from the
future and the replay guard still holds.
* fix(usage): remove daily.json's temp files when a write fails
updateDailyState writes daily.json through a temp file named
daily.json.<pid>.tmp and renames it into place. When the write or the
rename failed, for example a write cut short by a full disk or a rename
refused with EPERM while a Windows virus scanner or sync client holds
daily.json, the error went to the catch-all and the temp file stayed
behind. Every status line render is a new process with a new pid, so
each failing render left another orphan in ~/.cache/ccstatusline, and
nothing ever deleted them.
A failed write or rename now unlinks the temp file. A failed unlink is
ignored; as before, the state stays best-effort and the render goes on.
The same handle can be held on the temp file itself, and then it
refuses the unlink as well as the rename. So each write also removes
the other daily.json.<digits>.tmp files next to the state file once
they are a minute or more old, which no running write can still own.
While a handle stays held, only the last minute's temp files remain,
and orphans an older build left behind are cleared as well. The temp
name stays per process, so two renders never write through the same
file.
* fix(usage): keep today's extra usage spend across a token refresh
Extra Usage Today keeps each login's month-to-date total at the start of
the UTC day in daily.json, keyed by the usage cache's token fingerprint,
a hash of the refresh token. Claude Code saves a new refresh token each
time it refreshes the access token, which happens several times on a
working day. The key then changed mid-day, the new key had no record,
and today's spend started over from that moment: $15.00 at 13:00, then
$0.00 at 17:00 instead of $35.00 and $10.00 at 18:00 instead of $45.00.
The old record stayed behind until it was pruned.
The record is now kept under the account and organization the
credentials belong to. While a widget shows today's spend, ccstatusline
asks /api/oauth/profile through the usage fetch's own request, proxy
and credential code, once per token fingerprint, and keeps the answers
in ~/.cache/ccstatusline/usage-account.json. A failed lookup holds off
that fingerprint alone, for 30 seconds or a 429's Retry-After (at most
a day), and the hold-off is written before the request goes out, so
renders running side by side send one request between them.
Until the lookup answers, the record stays under the fingerprint as
before. Once the account is known, the records under its fingerprints,
written by an earlier build or while the lookup was failing, are folded
into the account's record once, so the day keeps its starting total.
A record seen while the clock ran ahead is dropped instead, as it would
be under its own key. A fingerprint that was never looked up can't be
placed with an account, so today's spend still starts over once when
an earlier build kept the record under a refresh token that has since
changed, or when a lookup outage spans a refresh.
The organization is part of the key because one account can sign in to
a personal organization and a work one, each with its own extra usage
total. The key is a hash, so neither cache file names the account.
* refactor(usage): tidy the account lookup and the record merge
mergeSpendRecord goes through an arrow, so reduce's index and array
arguments never reach it, and the account id is built before it's
fingerprinted instead of inside a nested template literal.
* refactor(usage): ask the profile endpoint once for the account email and the account key
The Claude Account Email widget (#724) and Extra Usage Today each asked
/api/oauth/profile about the same login, each with its own cache and backoff.
One request now answers both. lookUpProfile runs under the email lookup's
per-login lease and backoff files, and each caller keeps its part of the
answer in its own cache: the email in account-email-<hash>.json, the account
key in usage-account.json, which now holds only named logins. An answer that
leaves out the caller's part holds that login off as before, and the other
part is still kept.
A rate-limited lookup's Retry-After is kept within the day the backoff reader
trusts, as the account lookup did, so the email lookup no longer reads a
Retry-After further off as no backoff at all.
* docs(development): the account email and Extra Usage Today share one profile lookup
* refactor(usage): name the account id before fingerprinting it
The account key nested the id's template inside its own on one line. Build
the id first, as the account lookup did before it shared the profile
request.
* fix: enable label editing for Extra Usage Today
---------
Co-authored-by: Matthew Breedlove <sirmalloc@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #538
Problem
The Claude Account Email widget reads
oauthAccount.emailAddressfrom.claude.json. Claude Code writes that record at login and does not rewrite it when the credentials change underneath, so after an account switch the widget can keep naming the previous account. The usage widgets read the live credentials (getUsageCredentials()), so one status line can show one account's email next to another account's usage.Fix
fetchAccountEmail()inusage-fetch.tsgets the email fromGET /api/oauth/profile(account.email) with the usage credentials. It goes through the existing usage request code (same host,anthropic-betaheader,HTTPS_PROXY/NO_PROXYhandling and five-second deadline);fetchFromUsageApi()gained anapiPathparameter for this, and the usage request itself is unchanged.~/.cache/ccstatusline/account-email.json(mode 0600) under the usage cache's login fingerprint (getUsageCacheIdentity().preferredHash). An access-token refresh reuses the entry; only a login change triggers another request. Up to 10 logins are kept, so profiles used side by side don't evict each other.account-email.lockfor that login only: 30 seconds, or after a 429 the server'sRetry-After(five minutes without one, as for usage requests), with the same 24-hour bound asusage.lock.ccstatusline.tsruns the lookup next to the other prefetches, only when aclaude-account-emailwidget is configured, and passes the result asRenderContext.accountEmail.context.accountEmailand falls back to.claude.jsonwhen there is no OAuth login or no successful lookup yet, so API-key users and offline renders behave as before. Preview output is unchanged.docs/DEVELOPMENT.mdlists the new cache files and the widget's data source.docs/USAGE.mddescribes the lookup in the widget list, under Usage Credentials and Cache, and in the proxy settings.Notes for review:
.claude.json.Tests
src/utils/__tests__/usage-fetch.test.ts: the subprocess probe harness can now runfetchAccountEmailinstead offetchUsageData, and it records the request path andAuthorizationheader. One existing usage test now also checks that usage requests still go to/api/oauth/usage. NewfetchAccountEmailcases:/api/oauth/profilewith the live access token, and serves the second call from the cacheRetry-Aftersrc/widgets/__tests__/ClaudeAccountEmail.test.ts: the live email wins over.claude.json(onmainthis fails withExpected: "Account: current@example.com",Received: "Account: previous@example.com"),.claude.jsonis still used when the live email is null, and the live email shows when.claude.jsonhas none.Run on macOS with Bun 1.4.2, without network access:
bun run lint: cleanbun run build: OKbun test --timeout 20000: 3124 pass, 2 fail. The two failures are the "behind a proxy that never answers CONNECT" tests, which need to listen on a local socket; my environment does not allow that, and they fail the same way on unmodifiedmainhere (3110 pass, 2 fail).preserves root errors within a process...also times out here. That is not new: run on its own, it takes about 9 s both onmainand on this branch, and it passes on both with a longer timeout.End to end, with
.claude.jsonnamingprevious@example.com, credentials for another account, andhttps.requeststubbed to answer the profile request withcurrent@example.com:main: printsAccount: previous@example.comand makes no requestbun src/ccstatusline.tsandnode dist/ccstatusline.js: the first render makes oneGET /api/oauth/profilewith the live token and printsAccount: current@example.com; the second render uses the cache and makes no requestAccount: previous@example.comand writes the 30 s backoffRelated
#638 also changes
ClaudeAccountEmail.ts(it caches the.claude.jsonread by mtime). The two overlap inrender(); with this change.claude.jsonis read only on the fallback path. Whichever lands second will need a small rebase.