Desktop app: session context reports a stale identity from ~/.claude.json, not the account that actually owns and bills the session
Desktop app: session context reports a stale identity from ~/.claude.json, not the account that actually owns and bills the session
Correction (same day): originally filed claiming the inverse — that sessions billed to the stale account. Testing disproved that. Corrected version below.
Environment
- Claude desktop app (macOS, Darwin 25.5.0), embedded Claude Code 2.1.209
- App UI signed in as: personal account (Pro plan)
~/.claude.jsonoauthAccount: work account (Claude Team org) — stale, from an
earlier CLI login on the same machine
Summary
The desktop app login correctly controls session ownership and billing. But the
Claude Code session context reports a different, stale identity — theuserEmail injected into the model's context comes from ~/.claude.json'soauthAccount, a record left behind by the machine's original CLI login weeks
earlier, which the app never reconciles or updates.
Verified behavior (logout/login experiment)
- Signed into app with personal account: session usage panel shows
"Plan usage limits · Pro" with that plan's quotas and reset times.
- Signed out, signed into work (Team) account: panel shows
"Your usage limits · Team" with entirely different quotas/reset schedule.
The personal session is not accessible under this login.
- Signed back into personal: session reappears.
- Throughout all of this,
~/.claude.jsonoauthAccount(work identity,
profileFetchedAt timestamp byte-identical) never changed — the app never
touches it.
- Yet every session under the personal login has
userEmail: <work email>in
its system context.
So: billing ✓ correct, session scoping ✓ correct, identity in model context ✗ wrong.
Why this matters
- The model reasons from a false identity. In our session, Claude read the
work email from context, cross-checked ~/.claude.json (same stale record —
circular confirmation), and confidently concluded personal sessions were
billing the employer's Team seat. The user had to disprove Claude with
usage-panel screenshots. Wrong-identity context poisons anything downstream:
git identity suggestions, org/policy assumptions, personalization, memory.
- Users who inspect config are misled the same way.
~/.claude.jsonis the
documented home of the CLI's account state; a stale oauthAccount there looks
authoritative and implies billing goes somewhere it doesn't.
- No reconciliation. App login and the CLI credential record can drift
forever; nothing detects or surfaces the mismatch.
Expected behavior
- Session context
userEmailshould come from the live app login, or be omitted. - App login should update/reconcile (or invalidate)
~/.claude.json's
oauthAccount, or the stale record should be clearly marked non-authoritative.
Repro
- Log into Claude Code CLI (terminal) with account A →
~/.claude.jsonstores A. - Sign into the desktop app with account B.
- Start a Code session in the app → session is owned/billed by B (correct), but
its system context reports account A's email, and ~/.claude.json still
claims A indefinitely.
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
Correction: I've edited the title and body — the original filing claimed billing followed the stale
~/.claude.jsoncredential (i.e., personal sessions consuming the employer Team seat). A logout/login experiment disproved that: the usage panel showed the correct plan (Pro vs Team, different quotas and reset schedules) under each login, and sessions are only accessible under the login that created them. Billing and session ownership follow the app login correctly.What remains is the identity bug now described in the body: the session's system-context
userEmailcomes from a staleoauthAccountin~/.claude.json(left by an old CLI login, never reconciled by the app), so the model is told the wrong account identity. Notably, that false context caused Claude itself to confidently mis-diagnose the billing direction — the original, wrong version of this issue was drafted from that mis-diagnosis.Additional finding — the app already knows the correct identity; it's just not what gets injected into context.
The desktop app writes
lastKnownAccountUuidinto its own config at~/Library/Application Support/Claude/config.jsonon login. On this machine, after the logout/login cycle described above, that field holds the personal account's UUID (it matches theoauthAccount.accountUuida CLI login records for that account), while~/.claude.jsonstill holds the stale work identity. Org-scoped keys in the same file (dxt:allowlistLastUpdated:<orgUuid>) were refreshed for the work org and then the personal org 37 seconds apart, timestamping the account switch.So at session start the harness has a locally available, app-maintained record of the active account — but session context
userEmailis sourced from the stale~/.claude.jsonoauthAccountinstead.Workaround for affected users: ignore session-context
userEmailand~/.claude.json; readlastKnownAccountUuidfrom the app'sconfig.jsonand map it to your accounts (each account's UUID appears asoauthAccount.accountUuidafter a CLI login under that account).Suggested fix: populate session context from the same source the app already maintains — or omit
userEmailentirely when the app login and the CLI credential record disagree, rather than silently reporting the stale one.The context-injection staleness you documented shares its root cause with the
/status/banner display bug — root-caused with a deterministic repro in #79222: display and context surfaces read theoauthAccountcache in.claude.json(last writer wins:/login, plus a ~daily TTL-gated profile refresh), while the session's credential is resolved independently. YourlastKnownAccountUuidfinding is referenced there as a locally available source of truth the injection path ignores.