Skip to content

fix(services): identify accounts by accountUuid, not rotating refresh token#264

Open
AlvaroTena wants to merge 1 commit into
hamed-elfayome:mainfrom
AlvaroTena:fix/profile-switch-token-rotation
Open

fix(services): identify accounts by accountUuid, not rotating refresh token#264
AlvaroTena wants to merge 1 commit into
hamed-elfayome:mainfrom
AlvaroTena:fix/profile-switch-token-rotation

Conversation

@AlvaroTena

Copy link
Copy Markdown

Fixes #263

Problem

resyncBeforeSwitching used the OAuth refreshToken as an account-identity check (guard added in #215 to prevent cross-profile contamination). But Claude Code rotates the refreshToken on every token refresh within the same account, so fresh != stored is almost always true even for the same account. The guard therefore skipped the re-sync on essentially every switch, leaving the profile's stored credentials frozen at a now-invalidated token (and discarding the live rotated one). Once the stored access token expired, switching to that profile wrote dead credentials to the keychain → 401 Invalid authentication credentials → forced claude /login.

See #263 for the full root-cause analysis and a controlled A/B experiment reproducing both the failure and this fix.

Change

Identify accounts by the stable oauthAccount.accountUuid (already captured per-profile in oauthAccountJSON since #175) instead of the rotating refresh token. The re-sync is skipped only when account ids genuinely differ. When an account id is unavailable on either side, it falls back to the legacy refresh-token comparison, so behavior is unchanged for profiles that predate oauthAccountJSON capture.

  • New helper accountUUID(fromOAuthAccountJSON:).
  • resyncBeforeSwitching guard reworked to use account identity with a refresh-token fallback.

This is intentionally scoped to the primary fix (bug 1). The defense-in-depth follow-up discussed in #263 (refusing to write already-expired credentials over a working keychain in applyProfileCredentials) is left out of this PR to keep it focused.

Testing

  • xcodebuild ... -scheme "Claude Usage" buildBUILD SUCCEEDED (Xcode 26.5).
  • Reproduced the bug deterministically on the released build, then ran the same procedure against a local build of this branch:
    • Before: after forcing a real token rotation A → B and switching profiles, the stored profile token stayed at the dead A (rotation discarded).
    • After: same procedure rotated Y → Z; the stored profile token correctly became Z (rotation captured). No re-login required.

Notes

… token

resyncBeforeSwitching used the OAuth refreshToken as an account-identity
check. Claude Code rotates the refreshToken on every token refresh within
the same account, so the guard treated legitimate same-account switches as
'different account' and skipped the re-sync. The profile's stored
credentials froze at a now-invalidated token (and the live rotated one was
discarded), which later broke profile switching with HTTP 401.

Identify accounts by the stable oauthAccount.accountUuid instead, only
skipping the re-sync when account ids genuinely differ. Fall back to the
legacy refreshToken comparison when an account id is unavailable on either
side.

Fixes hamed-elfayome#263
chenwei791129 added a commit to chenwei791129/Claude-Usage-Tracker that referenced this pull request Jul 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant