Skip to content

claude-account-email reads a login-time snapshot, so it can name a different account than the usage widgets #538

Description

@steven-pribilinskiy

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions