Summary
The Claude Account Email widget can keep naming a previous account indefinitely after the active account changes.
It reads oauthAccount.emailAddress from .claude.json. Claude Code writes that block at login and doesn't refresh it when the credential underneath changes, so the widget is reporting a login-time snapshot rather than the account that is actually in use.
The usage widgets on the same status line read the live credential, so one line can show one account's email next to another account's usage percentages.
The two sources, both in ccstatusline
src/widgets/ClaudeAccountEmail.ts (L29-31): readFileSync(getClaudeJsonPath()) then data.oauthAccount?.emailAddress
src/utils/usage-fetch.ts: getUsageToken() reads .credentials.json from the same config dir
So "which account is this" is answered from two different places, and they can disagree.
What I saw
On a machine where the active account changed during the morning:
| source |
answer |
Claude Account Email widget |
a@example.com |
.credentials.json + GET /api/oauth/profile |
b@example.com |
| session/weekly usage widgets |
matched b@example.com |
oauthAccount.profileFetchedAt had not moved since before the change, so the block was simply never rewritten. The status line showed a@example.com beside percentages belonging to b@example.com.
Why it doesn't self-correct
.claude.json is held in memory by Claude Code and written back whole, so even fixing the field from outside gets undone by any session that started before the fix. Anything that changes the active credential without a fresh interactive login in every running session can land in this state, and stay there.
I don't think the widget can rely on that field tracking the credential.
Proposed fix
Resolve identity from the live credential, the one the usage path already uses:
GET https://api.anthropic.com/api/oauth/profile
Authorization: Bearer <accessToken from .credentials.json>
anthropic-beta: oauth-2025-04-20
{
"account": { "uuid": "...", "email": "...", "full_name": "..." },
"organization": { "name": "...", "organization_type": "claude_max" }
}
I checked this returns 200 with those fields.
usage-fetch.ts already has every piece: the same USAGE_API_HOST, the same bearer, the same anthropic-beta header, proxy support, and a disk cache keyed by token hash. Keying an identity cache the same way means it only refetches when the credential actually changes, so the added cost is roughly one request per login rather than per render.
Falling back to oauthAccount when the request fails seems fine, as long as it isn't treated as authoritative.
This was already proposed in #219
Worth saying up front that I'm not proposing anything new here. #219 (open since March) adds exactly this: an account-email widget reading /api/oauth/profile, with profile-fetch.ts and per-token cache files, and its motivation describes this same symptom.
The timeline is the awkward part:
| when |
what |
| 2026-03-11 |
#219 opened, reading the profile endpoint |
| 2026-04-08 |
#295 merged, adding ClaudeAccountEmail.ts reading .claude.json |
| today |
#219 is CONFLICTING and unmerged, main still reads the snapshot |
#219 adds a separate widget and never touches ClaudeAccountEmail.ts, so nothing in it fixes the widget people are actually using. Whether that's best resolved by reviving #219, by porting just its profile-fetch.ts into the existing widget, or by something smaller is a maintainer call. I'm filing this so the behaviour of the merged widget is written down somewhere, since #219 reads as a feature PR rather than a bug report and is easy to miss.
Relation to #504
#504 asks for plan/org so two accounts sharing one email can be told apart, sourced from the same oauthAccount block. Those exact fields come back in the profile response (organization.name, organization.organization_type), and read from the live credential they're correct per account rather than per last login. Fixing the source first would let #504 be built on data that follows the active account.
Relation to #536
#536 makes the same distinction for the usage cache: an access token is reissued on a short cycle, so it can't stand in for a login identity. Same reasoning, different consumer.
Happy to open a PR, just ask
Summary
The
Claude Account Emailwidget can keep naming a previous account indefinitely after the active account changes.It reads
oauthAccount.emailAddressfrom.claude.json. Claude Code writes that block at login and doesn't refresh it when the credential underneath changes, so the widget is reporting a login-time snapshot rather than the account that is actually in use.The usage widgets on the same status line read the live credential, so one line can show one account's email next to another account's usage percentages.
The two sources, both in ccstatusline
src/widgets/ClaudeAccountEmail.ts(L29-31):readFileSync(getClaudeJsonPath())thendata.oauthAccount?.emailAddresssrc/utils/usage-fetch.ts:getUsageToken()reads.credentials.jsonfrom the same config dirSo "which account is this" is answered from two different places, and they can disagree.
What I saw
On a machine where the active account changed during the morning:
Claude Account Emailwidgeta@example.com.credentials.json+GET /api/oauth/profileb@example.comb@example.comoauthAccount.profileFetchedAthad not moved since before the change, so the block was simply never rewritten. The status line showeda@example.combeside percentages belonging tob@example.com.Why it doesn't self-correct
.claude.jsonis held in memory by Claude Code and written back whole, so even fixing the field from outside gets undone by any session that started before the fix. Anything that changes the active credential without a fresh interactive login in every running session can land in this state, and stay there.I don't think the widget can rely on that field tracking the credential.
Proposed fix
Resolve identity from the live credential, the one the usage path already uses:
{ "account": { "uuid": "...", "email": "...", "full_name": "..." }, "organization": { "name": "...", "organization_type": "claude_max" } }I checked this returns 200 with those fields.
usage-fetch.tsalready has every piece: the sameUSAGE_API_HOST, the same bearer, the sameanthropic-betaheader, proxy support, and a disk cache keyed by token hash. Keying an identity cache the same way means it only refetches when the credential actually changes, so the added cost is roughly one request per login rather than per render.Falling back to
oauthAccountwhen the request fails seems fine, as long as it isn't treated as authoritative.This was already proposed in #219
Worth saying up front that I'm not proposing anything new here. #219 (open since March) adds exactly this: an
account-emailwidget reading/api/oauth/profile, withprofile-fetch.tsand per-token cache files, and its motivation describes this same symptom.The timeline is the awkward part:
ClaudeAccountEmail.tsreading.claude.jsonCONFLICTINGand unmerged,mainstill reads the snapshot#219 adds a separate widget and never touches
ClaudeAccountEmail.ts, so nothing in it fixes the widget people are actually using. Whether that's best resolved by reviving #219, by porting just itsprofile-fetch.tsinto the existing widget, or by something smaller is a maintainer call. I'm filing this so the behaviour of the merged widget is written down somewhere, since #219 reads as a feature PR rather than a bug report and is easy to miss.Relation to #504
#504 asks for plan/org so two accounts sharing one email can be told apart, sourced from the same
oauthAccountblock. Those exact fields come back in the profile response (organization.name,organization.organization_type), and read from the live credential they're correct per account rather than per last login. Fixing the source first would let #504 be built on data that follows the active account.Relation to #536
#536 makes the same distinction for the usage cache: an access token is reissued on a short cycle, so it can't stand in for a login identity. Same reasoning, different consumer.
Happy to open a PR, just ask