Repository navigation
[Bug]: Switching the Claude account with a live Claude terminal writes the target account’s expired token into the shared credential, and every running session dies with “Login expired” #23885
Description
Operating system
macOS
Orca version
v1.4.215
Details
Short summary
With at least one Claude terminal running, selecting a different managed Claude account copies that account’s stored OAuth credential into ~/.claude/.credentials.json and both Claude Code-credentials keychain slots without refreshing or validating it first. If the stored access token has expired and its refresh token no longer works, every running Claude session reads the dead credential. About 20 sessions across different worktrees died with Login expired. Please run /login within 90 seconds of one switch.
Related, but not the same path: #13165 (stale copy written over ~/.claude/.credentials.json) and #20251 (the live-PTY refresh gate never opens). This report is about the account-switch path specifically.
What happened (one machine, 2026-09-28, all times UTC. A read-only watcher logged the file mtime, expiresAt and keychain modification dates once a minute. No token values were read.)
The 22:34 row is the control. An expired access token alone is survivable, because Claude Code refreshes it. The switch is only fatal when the target account’s stored refresh token is also dead. Orca never checks that before writing it into the slot every session reads.
Where it happens (names from the minified 1.4.215 bundle, out/main/index.js)
In doSyncForCurrentSelection:
let l = Xp(); // Xp() = Hp.size > 0: any live Claude PTY
if (l && v9i(c)) this.managedRefreshDeferredByLivePtyAccountId = i.id;
if (!l) { let e = await this.refreshManagedAccountTokenIfNeeded(i, c); e && (c = e) }
// ...then unconditionally:
this.writeRuntimeCredentials(c); // ~/.claude/.credentials.json
await $e.l(c, u.configDir); // keychain: hashed slot AND bare "Claude Code-credentials"
v9i is “expires within 5 minutes”. With any live Claude PTY, the refresh is skipped, and the stored credential c is written to the shared runtime slots even when v9i(c) is true, meaning it has already expired. The deferral makes sense for the account that live PTYs are currently rotating. On a switch, though, the target is a different account whose refresh token no live PTY holds, so the reason for deferring does not apply.
The status-bar account menu already refreshes expiring inactive tokens when it opens (rateLimits:fetchInactiveClaudeAccounts refreshes and persists them). That fetch is throttled to once every 60s. A switch does not wait for it, and it does not check whether it succeeded.
How to reproduce
- Add two managed Claude accounts, A and B. Let B sit unused for more than 8 hours so its stored access token expires. To make the failure fatal rather than self-healing, make B’s stored refresh token stale. Here that happened after another tool refreshed B outside Orca.
- With A active, start one or more
claudesessions in Orca terminals and leave them running. - Select B from the status-bar account menu or from Settings > Accounts.
- Within about a minute, the running sessions fail with
Login expired. Please run /login (authentication_failed).~/.claude/.credentials.jsonholds aclaudeAiOauth.expiresAtin the past.
Expected
On a switch, Orca should make sure the target credential is live before writing it into the shared slots:
- If the target token is expired or expiring, refresh it even when live PTYs exist. No live PTY is rotating the target account’s lineage.
- If the refresh fails, do not materialize the dead credential. Keep the current account, and show “Re-authenticate B” on the account row.
Also observed, less certain
At 00:13 on the same night, with no account switch, ~/.claude/.credentials.json and the hashed keychain slot were deleted, then re-created 15 to 30 seconds later with the same token. Four sessions died with Login expired inside that window. The only code I found that removes both is clearRuntimeAuthForAccount / restoreSystemDefaultSnapshot. I have not established what triggered it. A write that replaces the credential in place, with no delete first, would close that window too.
Anything else that might help
- About 20 concurrent
claudesessions, all launched from Orca terminals. Claude Code reads the bare keychain slot or theCLAUDE_CONFIG_DIR-hashed slot. - I can share the watcher log. It holds timestamps, expiry times and 8-hex token fingerprints, never token values.
Activity
OrcaWin commentedon Oct 11, 2026
Thanks for reporting. This is fixed on main by #26801: each managed Claude account now runs from its own folder, so switching accounts no longer rewrites a shared credential under running sessions.