Skip to content

[Bug]: Account sync rejects with "live connector credentials conflict" when two Claude keychain items hold different MCP tokens; blocks Claude launches and usage badge #27334

Closed

Description

@jjongsta

Summary

Every account-sync tick in Orca's main process rejects with Cannot switch Claude accounts: live connector credentials conflict; existing authorizations were preserved. The rejection is unhandled (main_unhandled_rejection crash breadcrumb), it fires 10-15 times per hour, and while it persists (a) Orca cannot launch any Claude terminal from the app or from orca terminal/worktree flows for that account, and (b) the account usage badge in the bottom bar stays at "..." for every account.

Environment

  • Orca 1.4.223, macOS (Darwin 25.6.0), local runtime (not orca serve)
  • 2.1.296 (Claude Code)
  • 3 managed Claude subscription accounts (host auth runtime), one selected
  • Many long-running Claude sessions on the machine (12-16 claude processes, started both by Orca and by hand)

What the trace shows

~/Library/Application Support/Orca/logs/main.trace.ndjson, crash breadcrumb every few minutes:

Error: Cannot switch Claude accounts: live connector credentials conflict; existing authorizations were preserved
    at Koo
    at qoo
    at Joo
    at dso.mergeLiveRuntimeSharedCredentials
    at async dso.doSyncForCurrentSelection

Count per UTC hour on 2026-10-10: 17h 2, 18h 15, 19h 1, 20h 12, 21h 13, 23h 10.

Root cause as far as I can tell

Claude Code keeps two keychain items for the same login on this machine:

  • the legacy item Claude Code-credentials
  • the per-config-dir item Claude Code-credentials-<sha256(configDir)[:8]> (here the suffix matches ~/.claude)

Both hold the same claudeAiOauth login (same access token), but their mcpOAuth sections (the MCP connector tokens) drift apart: the live sessions refresh the legacy item (last refresh 23:30Z), the per-dir item stopped at 17:47Z. Six older per-dir items from previous config dirs also exist, some with empty connector tokens. mergeLiveRuntimeSharedCredentials sees different connector tokens for the same connector key across these "live" sources and refuses the whole sync instead of taking the freshest (latest expiresAt) token per key.

Impact

  • Automated Claude launches are refused ("Cannot switch Claude accounts"), so every new agent terminal needs a human to type claude ... in a plain shell tab.
  • The usage/rate-limit badge never renders because the sync that precedes it never completes.
  • Re-authenticating the account in Orca and restarting Orca do not clear it (tried both).

Expected

  • The merge takes the connector token with the latest expiresAt per connector key (or the one from the selected account's own config dir), logs the disagreement, and completes the sync.
  • A conflict in connector tokens should never block launching a terminal or fetching usage; those only need the claudeAiOauth login, which agrees across the stores.
  • The error should surface in the UI (account menu) instead of only as an unhandled rejection in the trace.

Steps to reproduce (best guess)

  1. Use one Claude subscription account from both an Orca-launched session and a hand-started claude with the default config dir, with at least one OAuth MCP connector configured.
  2. Let the connector tokens refresh in one session only (a few hours of use).
  3. Watch main.trace.ndjson for crash.breadcrumb with the error above; try to launch a new Claude terminal from Orca.

Happy to run a debug build or share a redacted trace excerpt.

Activity

  1. jjongsta commented on Oct 10, 2026

    @jjongsta
    Author

    Follow-up with the actual pair that conflicts, after two experiments:

    1. Deleting the seven per-config-dir keychain items (Claude Code-credentials-<hash>) did NOT stop it: the next sync tick (3 minutes later) rejected again with only the legacy keychain item left.
    2. Moving the plaintext file ~/.claude/.credentials.json aside DID stop it: 0 rejections in the 6 minutes after the move, against one every 3-5 minutes before.

    That file was last written at 17:42Z while the legacy keychain item kept being refreshed by the running sessions (connector tokens for the same MCP servers differ between the two). Orca's claude-runtime-auth/system-default-auth.json carries credentialsJson, legacyKeychainCredentialsJson and scopedKeychainCredentialsJson, so I take it the merge compares the file store with the keychain stores and refuses when their mcpOAuth entries differ.

    Suggested fix stays the same: per connector key, take the token with the latest expiresAt (or prefer the keychain on macOS, where Claude Code writes), and never let a connector-token disagreement block account switching or the usage fetch.

  2. santidevhmo commented on Oct 11, 2026

    @santidevhmo

    Same bug here on Orca 1.4.224, macOS 26.4.1 (arm64). It started after a force quit and a reboot, and every Claude launch failed with "Couldn't launch Claude — the terminal did not start" plus this conflict error. Adding a second data point, because moving the plaintext file aside was not enough on my machine, and a workaround that did fix it.

    What I found

    1. I moved ~/.claude/.credentials.json aside first, as suggested above. Its last write was the day before, and it held 19 mcpOAuth entries. Launches still failed afterwards.
    2. Two keychain items remained: the legacy Claude Code-credentials (refreshed today by running sessions) and the per-config-dir Claude Code-credentials-6fb7661b, where 6fb7661b is sha256("$HOME/.claude")[:8] (last written the evening before).
    3. Comparing their mcpOAuth maps, without printing tokens, showed 6 of 19 connectors with different grants: atlassian, linear-server, cloudflare, context7, mobbin and supabase. In every case where both sides had an expiresAt, the legacy item's was later. The per-dir item's tokens had already expired.

    That matches resolveCredentialField in src/main/claude-accounts/shared-credential-fields.ts (added in #21931): when two live sources both differ from the baseline for the same server key and from each other, it throws instead of choosing one. So any drift between the stores eventually blocks every launch.

    Workaround that fixed it

    Back up the stale per-dir item, then overwrite it with the legacy item so both stores agree. Launches worked right after.

    # Run with: python3 -I sync_claude_keychain.py
    # Replace the suffix with your own: printf '%s' "$HOME/.claude" | shasum -a 256 | cut -c1-8
    import re, subprocess
    
    L, S = "Claude Code-credentials", "Claude Code-credentials-6fb7661b"
    B = S + "-backup"
    
    def acct(s):
        r = subprocess.run(["security", "find-generic-password", "-s", s], capture_output=True, text=True, check=True)
        return re.search(r'"acct"<blob>="([^"]*)"', r.stdout).group(1)
    
    def pw(s):
        r = subprocess.run(["security", "find-generic-password", "-s", s, "-w"], capture_output=True, text=True, check=True)
        return r.stdout.rstrip("\n")
    
    a, old = acct(S), pw(S)
    subprocess.run(["security", "add-generic-password", "-U", "-s", B, "-a", a, "-w", old], check=True)
    assert pw(B) == old, "backup mismatch, stopping before overwrite"
    subprocess.run(["security", "add-generic-password", "-U", "-s", S, "-a", a, "-w", pw(L)], check=True)
    print("items identical now:", pw(S) == pw(L))

    Caveats: this only syncs the stores for now. Running sessions keep refreshing just the legacy item, so the two drift apart again and the script has to be re-run. It also passes the credential JSON as a security argument, so it is briefly visible in ps on the local machine.

    Fix suggestion

    +1 to resolving per server key instead of throwing. When candidates conflict, keep the grant with the latest expiresAt (the access/refresh pair stays atomic because the whole grant object is chosen), and log the disagreement. A connector conflict should never block a launch, because the claudeAiOauth login agreed across all stores in my case too. Happy to test a build.

  3. ggbdpq commented on Oct 11, 2026

    @ggbdpq
    Contributor

    For anyone landing here from a search: the rejecting comparison does not exist on current main anymore. It came in with #21931 (shared-credential-fields.ts, "resolve per server key or throw"), and the per-account folders re-land #26801 (Oct 9) removed that module and the shared-credential sync entirely, so main has no "live connector credentials conflict" throw left. Builds cut before the re-land (including 1.4.223/1.4.224) still carry it. If you can reproduce this on a build that includes #26801, please comment with the version and I or someone else can reopen the investigation.

  4. nwparker commented on Oct 11, 2026

    @nwparker
    Contributor

    Already fixed on main by #26801 , which removes the code that throws. It ships in the next release.

    Sorry for the issues!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions