Skip to content

fix(claude-accounts): write oauthAccount to the config file Claude reads - #22668

Closed
AlgoraAndrew wants to merge 2 commits into
stablyai:mainfrom
AlgoraAndrew:AlgoraAndrew/fix-claude-runtime-config-path
Closed

AlgoraAndrew wants to merge 2 commits into
stablyai:mainfrom
AlgoraAndrew:AlgoraAndrew/fix-claude-runtime-config-path

Conversation

@AlgoraAndrew

@AlgoraAndrew AlgoraAndrew commented Sep 24, 2026 •

Copy link
Copy Markdown

ELI5

When you switch Claude accounts, Orca updates two things: the login token and a small "who am I" record (oauthAccount). It wrote that record to a file Claude doesn't read, so new terminals kept showing the old account. Now Orca writes it where Claude actually looks.

What Changed

  • ClaudeRuntimePathResolver uses $CLAUDE_CONFIG_DIR/.claude.json only when CLAUDE_CONFIG_DIR is inherited, and otherwise ~/.claude.json. This matches how Claude Code chooses its config file. The existence of ~/.claude/.claude.json no longer redirects Orca's writes. Like Claude, the resolver prefers a legacy <config dir>/.config.json when it exists (added in review, thanks to pullfrog).
  • The system-default auth snapshot now records the configPath it was captured from.
  • A snapshot captured from a different path, or written before this change, cannot prove which identity was the system's. It may hold the identity from ~/.claude/.claude.json, a file Claude never read. Before any runtime mutation, reconcileSystemDefaultSnapshotConfigPath therefore sets that snapshot's configOauthAccount to null and records the current path.
    • Returning to the system default then removes oauthAccount through the existing ownership-checked restore, instead of writing a possibly wrong identity.
    • Claude Code repopulates a missing oauthAccount at startup from GET /api/oauth/profile with its current token.
    • This runs at most once per path change, because new snapshots record their path.

Before: after an account switch, claude auth status in new terminals showed the previous account whenever ~/.claude/.claude.json existed.
After: new terminals show the selected account. Returning to the system default never restores a stale identity: at worst oauthAccount is absent until Claude's next start refetches it.

Why

Claude Code decides which config file to read by whether CLAUDE_CONFIG_DIR is set, not by which file exists. I verified this on 2.1.281: with different identities in the two files, the CLI reports ~/.claude.json when the variable is unset and $CLAUDE_CONFIG_DIR/.claude.json when it is set. Tools that run Claude with CLAUDE_CONFIG_DIR=~/.claude (for example claude-mem's observer, thedotmack/claude-mem#4149) leave the colocated file behind permanently.

For snapshots taken under the old resolver, I first tried to infer the source and recapture the system identity from the current file. Independent review kept finding histories where the recaptured value was not the system identity: a system identity equal to a managed one, a colocated file created after the snapshot, a changed CLAUDE_CONFIG_DIR, removed accounts, email casing, and unreadable or WSL-probed managed storage. Nothing on disk proves which identity was the system's. Clearing is the only choice that can never restore a wrong identity, and Claude self-heals it from the token's profile.

Linked Issue

Fixes #22667

Visual Proof

N/A. No UI change; the behavior is covered by the tests below and the CLI probe in the issue.

Testing

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

  • npx vitest run src/main/claude-accounts: 29 files, 240 tests pass.

  • pnpm typecheck exits 0. oxlint and oxfmt pass (also via the pre-commit hook).

  • New tests:

    • runtime-paths.test.ts: the home config stays in use when a colocated file exists without an override; the colocated file is used when CLAUDE_CONFIG_DIR is inherited; a legacy .config.json takes precedence in both cases.
    • runtime-auth-service-config-path.test.ts: materialization and restore update only the file the CLI reads, with and without an explicit directory.
    • runtime-auth-service-config-path-upgrade.test.ts: a stale identity is never restored for legacy or path-changed snapshots. The histories covered are:
      • a nested source;
      • a system identity equal to the managed one;
      • a first post-upgrade Keychain write failure;
      • a root source;
      • a nested file created after the snapshot;
      • a newly inherited CLAUDE_CONFIG_DIR;
      • a two-account config-path round trip, including with unreadable managed storage;
      • native reconciliation on Windows when a WSL ownership probe times out.
  • Disabling the reconciliation makes the upgrade tests fail; reverting the resolver change makes the resolver and config-path tests fail.

  • Platform: macOS. Windows and WSL behavior is covered by unit tests only. WSL managed accounts keep their own CLAUDE_CONFIG_DIR, and reconciliation runs only for the host runtime.

  • Manual check: after a local repair to the same state, a fresh claude auth status reported the selected managed account.

AI Disclosure

Diagnosis and implementation by Claude Opus 5.5 (Claude Code). Independent review by GPT-6 Astra (Codex) in fresh sessions, with human direction and approval.

Review

Six independent review rounds (GPT-6 Astra, a fresh session each). Rounds 1–5 raised R1–R9, all on snapshot migration; each was fixed or made moot by the final clear-on-unknown policy. Round 6 reported no findings of any severity and confirmed that R1–R9 no longer apply. A seventh round on the .config.json follow-up also reported no findings.

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred). Locally: pnpm typecheck, oxlint/oxfmt on the changed files, and the full src/main/claude-accounts suite. Full lint, test and build are left to CI.

🤖 Generated with Claude Code

Orca resolved the Claude config path to ~/.claude/.claude.json whenever that
file existed, but Claude Code reads it only when CLAUDE_CONFIG_DIR is set and
otherwise reads ~/.claude.json. Tools that run Claude with
CLAUDE_CONFIG_DIR=~/.claude leave the colocated file behind, after which every
managed account switch moved the credentials but wrote oauthAccount to a file
the CLI never reads, so new terminals kept showing the previous account.

Resolve the colocated file only when CLAUDE_CONFIG_DIR is inherited. The
system-default snapshot now records its configPath. A snapshot captured from
another path, or before paths were recorded, cannot prove which identity was
the system's, so its oauthAccount is cleared before any runtime mutation and
Claude repopulates it from its token's profile on the next start.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: e70a801f-803f-48fa-b309-d164f7756ca4

📥 Commits

Reviewing files that changed from the base of the PR and between 6912c73 and c46d6ed.

📒 Files selected for processing (2)
  • src/main/claude-accounts/runtime-paths.test.ts
  • src/main/claude-accounts/runtime-paths.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

The runtime path resolver selects an existing .config.json in the config directory when present. It selects the colocated .claude.json when CLAUDE_CONFIG_DIR is set, and otherwise uses the home config file. System-default snapshots record the config path and are reconciled when that path changes. Tests cover config updates, restoration, and legacy snapshots without a recorded path.

Merge Risk: ⚪ Minimal · up to c46d6

Account updates and restoration target the selected Claude config, with snapshots reconciled when that path changes. The supplied changes show no remaining merge-blocking risk.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The pull request satisfies the coding requirements in [#22667]. ClaudeRuntimePathResolver selects ~/.claude.json when CLAUDE_CONFIG_DIR is unset, selects the colocated config when the variable i…
Out of Scope Changes check ✅ Passed The changes stay within [#22667]. Production changes update Claude config-path selection, snapshot provenance, stale-identity handling, and related sync or rollback paths. The added tests verify these…
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 9 files.
Title check ✅ Passed The title clearly and concisely describes the main change: writing oauthAccount to the configuration file that Claude reads.
Description check ✅ Passed The description is complete and relevant. It covers the user impact, implementation, rationale, linked issue, visual proof, testing, platform coverage, review, and checklist status. It also explains t…

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: fcbcef96-b0f8-4b09-8800-de615510a8c3

📥 Commits

Reviewing files that changed from the base of the PR and between 122b8c2 and 6912c73.

📒 Files selected for processing (9)
  • src/main/claude-accounts/runtime-auth-service-config-path-upgrade.test.ts
  • src/main/claude-accounts/runtime-auth-service-config-path.test.ts
  • src/main/claude-accounts/runtime-auth-service.ts
  • src/main/claude-accounts/runtime-auth/runtime-auth-keychain-snapshots.ts
  • src/main/claude-accounts/runtime-auth/runtime-auth-snapshot-capture.ts
  • src/main/claude-accounts/runtime-auth/runtime-auth-sync.ts
  • src/main/claude-accounts/runtime-auth/runtime-auth-types.ts
  • src/main/claude-accounts/runtime-paths.test.ts
  • src/main/claude-accounts/runtime-paths.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ Minor suggestion only — the resolver now matches Claude's .claude.json rule, but not the .config.json case.

Reviewed changes

  • Resolver now keys off CLAUDE_CONFIG_DIR — resolveConfigPath returns <configDir>/.claude.json only when the variable is inherited, otherwise ~/.claude.json; the existsSync fallback that let a colocated file redirect writes is gone.
  • Snapshots record their source file — configPath added to ClaudeSystemDefaultSnapshot (runtime-auth-types.ts:18), written at capture (runtime-auth-snapshot-capture.ts:93), and accepted as optional by the shape validator (runtime-auth-keychain-snapshots.ts:24) so pre-upgrade snapshots stay readable.
  • New reconcileSystemDefaultSnapshotConfigPath() — rewrites a snapshot whose recorded configPath differs (including undefined) with configOauthAccount: null plus the current path; runs before host mutation in doSyncForCurrentSelection and in forceMaterializeCurrentSelectionForRollback.
  • Test coverage — two new files pin the resolver rule, materialize/restore per config path, and a set of legacy/path-changed snapshot migration histories with exact config contents.

I verified the load-bearing premise against the shipped @anthropic-ai/claude-code bundles: Claude's global-config resolver is existsSync(join(CLAUDE_CONFIG_DIR ?? ~/.claude, '.config.json')) ? that : join(CLAUDE_CONFIG_DIR ?? homedir(), '.claude.json'). That confirms the unset case reads ~/.claude.json and never the nested .claude.json, and that every host restore site is preceded by the reconcile. The suite passes (29 files, 238 tests). The one residual gap is inline.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Claude Code reads <config dir>/.config.json before .claude.json when it
exists (config dir = CLAUDE_CONFIG_DIR, else ~/.claude). Mirror that rule so
oauthAccount lands in the file Claude reads in that case too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

This run covers the delta since the prior pullfrog review — commit c46d6ed (honor Claude's legacy .config.json precedence), which closes the one finding from that review.

  • Resolver honors Claude's legacy .config.json precedence — resolveConfigPath (runtime-paths.ts:27) now probes <configDir>/.config.json first, then falls back to the colocated .claude.json when CLAUDE_CONFIG_DIR is inherited and ~/.claude.json otherwise. This mirrors Claude's exact rule existsSync(join(CLAUDE_CONFIG_DIR ?? ~/.claude, '.config.json')) ? that : join(CLAUDE_CONFIG_DIR ?? homedir(), '.claude.json') across all four CLAUDE_CONFIG_DIR / file-existence states. Snapshot capture and restore go through the same resolver, so the fixed path is used consistently.
  • Resolver test covers both override states — the new parameterized case (runtime-paths.test.ts:125-142) writes both candidate files and asserts an exact configPath, which fails under the previous implementation in both branches; ran it locally (11 tests pass).

The prior review's thread on runtime-paths.ts:35 is resolved, and no new findings surfaced.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@nwparker

Copy link
Copy Markdown
Contributor

Thanks for putting this together, and sorry for the slow turnaround. The area has since been reworked and this is now covered on main by #26801 (per-account CLAUDE_CONFIG_DIR and scoped Keychain items, replacing the old credential swap/OAuth refresh code), so we're closing this as superseded. If you still see the problem on the latest build, please open a fresh report or reopen with a current repro. Thank you for the contribution.

@nwparker nwparker closed this Oct 24, 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

Development

Successfully merging this pull request may close these issues.

[Bug]: Account switch writes oauthAccount to ~/.claude/.claude.json, so new Claude terminals show the previous account

2 participants