Skip to content

feat: per-worktree Claude account binding (concurrent multi-account)#6963

Open
srs-adamr wants to merge 14 commits into
stablyai:mainfrom
srs-adamr:srs-adamr/feat-per-worktree-claude-account
Open

feat: per-worktree Claude account binding (concurrent multi-account)#6963
srs-adamr wants to merge 14 commits into
stablyai:mainfrom
srs-adamr:srs-adamr/feat-per-worktree-claude-account

Conversation

@srs-adamr

@srs-adamr srs-adamr commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Lets each worktree bind to a specific Claude managed account, so multiple worktrees can run different Claude accounts concurrently, and a worktree's agents stay deterministically pinned to its account across spawn/despawn churn. Closes the per-project ask in #5203 / #5635 (concurrency is the superset). Tracking design discussion: #6921.

Today account selection is global per host (activeClaudeManagedAccountId), and the host launch path materializes the active account's creds into shared ~/.claude, which is structurally single-tenant. This PR adds an optional claudeAccountId on the worktree and, when set, has the launch reuse Orca's existing WSL-style CLAUDE_CONFIG_DIR injection on the host path instead of materializing — so assigned worktrees are isolated and concurrent. Purely additive: unassigned worktrees are byte-for-byte unchanged.

What's included:

  • Data modelclaudeAccountId?: string | null on Worktree + WorktreeMeta (mirrors the workspaceStatus precedent end-to-end through persistence/IPC/store).
  • Launchpty.ts resolves the worktree's pinned account fresh at every spawn and passes it as an override; getPreparation returns the injection shape ({ configDir: managedAuthPath, envPatch: { CLAUDE_CONFIG_DIR }, stripAuthEnv: true }) for host and WSL pinned accounts; doSyncForCurrentSelection early-returns for injected accounts (no materialization/read-back).
  • macOS — seeds only the config-dir-scoped Keychain service (Claude Code-credentials-<sha256(configDir)[:8]>) once per launch; never the shared legacy service.
  • Concurrency — injected launches are exempted from the global account-switch gate (they don't touch the shared runtime); the non-injected path keeps the gate exactly as-is.
  • UI — account selector in the create-worktree modal + an "Assign Account" submenu in the worktree context menu, both filtered by the worktree's runtime (host vs WSL/distro).

Screenshots

Create-worktree modal — new Account selector beside the Agent picker (hidden when no accounts exist; runtime-filtered host/WSL):

orca-pr-modal

Worktree context menu — new Assign Account submenu ("Inherit global" + managed accounts):

orca-pr-context-menu

Testing

  • pnpm typecheck — clean
  • pnpm build — clean
  • pnpm lint — the only failure is pre-existing verify:localization-catalog debt in EphemeralVmsPane.tsx (a file this PR does not touch); this PR's own new i18n keys pass.
  • pnpm test — 16 failures, all pre-existing on main (PR-comments, Linear-skill toast, sidebar-toolbar suites — all outside this PR's diff). Verified by running the full suite at the base commit: identical 16 failures there (16 failed / 23035 passed) vs this branch (16 failed / 23039 passed) → this PR adds zero new failures. Branch is rebased onto current upstream/main (v1.4.111-rc.2); our targeted suites are green (347 passed across claude-accounts, pty.test.ts, and the composer card).
  • Added/updated high-quality tests: new unit + integration tests for host & WSL injection, runtime/distro-mismatch fallback + warn, the macOS scoped-keychain seed, and the switch-block exemption (injected launch not blocked during a global switch; non-injected still blocked). The previously-broken pty.test.ts mock (getWorktreeMeta) is fixed.

AI Review Report

Reviewed with Claude (Opus) across the full diff. Main risks checked:

  • Additive/no-regression — verified unassigned worktrees take an identical code path; the switch-block change reduces to the exact original condition when !isInjectedClaudeLaunch. The override is threaded as a trailing optional param, leaving ~130 existing call sites untouched.
  • Cross-platform (macOS / Linux / Windows + WSL/SSH-remote) — explicitly confirmed: all Keychain operations are process.platform === 'darwin'-guarded (no-ops elsewhere); host vs WSL paths are cleanly separated via managedAuthRuntime/wslLinuxAuthPath; runtime filtering reuses the existing getWslDistroFromPath helper (no hardcoded path separators); SSH-remote spawns are correctly out of scope (no-op fallback, never a wrong-account launch). No Electron menu accelerators or shortcuts touched.
  • Refresh/rotation — confirmed Orca does no proactive refresh for injected accounts (doSyncForCurrentSelection early-returns), so token rotation ([Feature]: Make Orca's managed Claude (cloud subscription) auth survive token rotation — stop daily re-auth #6234) is preserved; multi-tenant (N worktrees → 1 account) refresh is CLI-owned per config dir, as it already is for the shared runtime today.

Flagged and addressed: an initial version left WSL bindings inert (host-only resolution) — fixed so WSL worktrees inject their account's wslLinuxAuthPath, with a runtime/distro-mismatch fallback + warn.

Security Audit

  • Credentials by reference, never by value — injection only ever puts a path (CLAUDE_CONFIG_DIR) in the child env and sets stripAuthEnv: true; no token/credential is ever placed in the environment or logged (the mismatch/seed warnings log account ids, not secrets).
  • Path jailing — injection requires getOwnedManagedAuthPath(account) (ownership-marker validated), so a pinned account can't point CLAUDE_CONFIG_DIR outside Orca's managed root.
  • IPC — the create/update-meta IPC surface carries only the claudeAccountId string; no credential material crosses IPC.
  • macOS Keychain — seeds only the scoped service for the account's own config dir; the shared legacy Claude Code-credentials service is never written from an injected launch (avoids cross-worktree clobber), and the seed is best-effort (degrades to Claude's own login prompt, never throws).
  • No new dependencies; no command execution added.

Follow-up needed: none blocking. See Notes for deferred scope.

Notes

  • Platform: macOS requires Claude Code 2.1+ (config-dir-scoped Keychain). Linux/Windows/WSL are file-based and need no Keychain step.
  • Deferred (intentional, documented): (1) a full per-CLAUDE_CONFIG_DIR refresh lock — not needed for this PR since Orca doesn't proactively refresh injected accounts (CLI-owned); (2) per-worktree usage/rate-limit display in the Accounts pane.
  • Binding home: stored on WorktreeMeta following the workspaceStatus precedent — happy to relocate if maintainers prefer a different home (raised in [Feature]: Per-worktree Claude account binding (concurrent multi-account + per-project defaults) #6921).

Open in Stage

@stage-review

stage-review Bot commented Jun 30, 2026

Copy link
Copy Markdown

Ready to review this PR? Stage has broken it down into 6 individual chapters for you:

Title
1 Define per-worktree Claude account data model
2 Implement account injection and runtime validation
3 Wire account injection through PTY spawn
4 Persist pinned account in worktree metadata
5 Add UI for worktree account selection
6 Wire account selection through creation flow
Open in Stage

Chapters generated by Stage for commit 30b3b50 on Jul 2, 2026 2:10am UTC.

srs-adamr and others added 11 commits July 1, 2026 06:43
Wave 1 plumbing for per-worktree Claude account binding. Adds the
optional claudeAccountId field to Worktree and WorktreeMeta (following the
workspaceStatus precedent), surfaces it through the meta->Worktree merges
(git + folder paths), and threads it through createWorktree args. Persistence
(blind spread merge) and the renderer store (generic Partial<WorktreeMeta>
spread) carry the field with no further change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve the worktree's claudeAccountId fresh at each Claude PTY spawn
(pty.ts) and pass it as an overrideAccountId on the selection target. In
getPreparation, a valid owned host override returns the WSL-shaped injection
({configDir, CLAUDE_CONFIG_DIR envPatch, stripAuthEnv}) instead of
materializing into shared ~/.claude, and doSyncForCurrentSelection
early-returns for injected accounts (mirroring the WSL path). A missing,
foreign, or WSL override is ignored, so unassigned worktrees keep today's
global behavior — purely additive.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude Code 2.1+ reads macOS Keychain credentials from a config-dir-scoped
service rather than the legacy unscoped one. An injected per-worktree host
account points CLAUDE_CONFIG_DIR at its own managedAuthPath, so before launch
we now seed ONLY that scoped service (reusing the existing scoped-only
writeActiveClaudeKeychainCredentials helper) from the account's managed
credentials. The legacy unscoped service is never touched here — that's the
shared global-selection singleton, and writing it from multiple worktrees
would race. No-op on Linux/Windows (keychain ops are darwin-only there) and
for unassigned worktrees, which keep today's behavior unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Threads the per-worktree claudeAccountId (Wave 1 data model) from the
quick-composer's submitQuick(agent, claudeAccountId) through
WorktreeCreationRequest and into the createWorktree store action's trailing
options bag (alongside automationProvenanceRequest, avoiding yet another
positional parameter on an already 24-deep signature). Purely additive:
existing callers omit the new param/field and see no behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a Claude account picker beside the Agent picker in the quick-create
modal, mirroring the quickAgent/onQuickAgentChange controlled-prop pattern.
Populated from claudeAccounts:list, default "Inherit global", filtered to
accounts compatible with the target repo's runtime (host vs WSL — keyed off
the repo's on-disk path via the new claude-account-runtime-filter helper,
shared with the upcoming context-menu surface). Hidden for folder-workspace
and SSH-remote targets, where the field either isn't forwarded yet or has no
local launch path to apply to.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Clones the "Move to Status" DropdownMenuSub into an "Assign Account" submenu
(plus "Inherit global") calling updateWorktreeMeta(id, { claudeAccountId }).
Fetches claudeAccounts:list only while the menu is open, mirroring the
existing selectMenuScopedMap gating so many closed sidebar rows don't each
fire an IPC call. Filtered by runtime (host vs WSL) via the
claude-account-runtime-filter helper added alongside the create-modal
selector; multi-select uses the first selected worktree's path as the
representative runtime, matching contextWorkspaceStatus's "use first" mixed-
value convention.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n mismatch

resolveInjectedHostAccount only resolved HOST override accounts, so a
worktree pinned to a WSL account had no effect on WSL launches: the WSL
prep branch in getPreparation kept reading the global active account's
wslLinuxAuthPath instead of the worktree's override. Generalize the
resolver (now resolveInjectedAccount) to also resolve a WSL override
whose runtime/distro matches the launch, inject that account's
wslLinuxAuthPath (+ configDir/wslLinuxConfigDir/wslDistro, stripAuthEnv)
from getPreparation, and bypass materialization in
doSyncForCurrentSelection the same way the host path already does.

On a runtime/distro mismatch (e.g. a host-pinned account on a WSL
launch, or a WSL-pinned account for the wrong distro), fall back to the
global selection and emit a single console.warn instead of silently
ignoring the pin. seedInjectedHostAccountKeychain now explicitly skips
WSL accounts (they're isolated by their own Linux config dir and need
no Keychain seed), since the generalized resolver can return either
runtime.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wave 1 added a store.getWorktreeMeta(worktreeId) call in pty.ts to resolve
the per-worktree claudeAccountId override at spawn time, but the store
mocks in pty.test.ts were never given that method, causing
"store.getWorktreeMeta is not a function" in 13 tests whenever a spawn
path passed a string worktreeId. Add getWorktreeMeta: vi.fn() (returning
undefined, i.e. no pinned account) to each affected store mock.
… global switch-block

doSyncForCurrentSelection already early-returns for injected (per-worktree-
pinned) accounts, so Orca does no proactive refresh/materialization for
them — the Claude CLI owns each injected config dir's own token refresh.
The only remaining gap was that a Claude launch in a pinned worktree could
still be blocked by isClaudeAuthSwitchInProgress() (live-pty-gate.ts), even
though it never reads/writes the shared ~/.claude runtime that gate
protects.

Add ClaudeRuntimeAuthService.hasInjectedAccountOverride(), a synchronous
wrapper around the existing resolveInjectedAccount() resolution (the same
one getPreparation() uses), so the PTY spawn gates in pty.ts can tell
whether an in-flight launch is injected *before* paying for the async
prepare/keychain work. Both gates in pty.ts (runtime.setPtyController's
spawn and the pty:spawn IPC handler) now skip the switch-block throw only
when the launch resolves a valid per-worktree override; the global-
selection (non-injected) path is unchanged.

This is explicitly NOT the per-config-dir refresh lock described in the
spec's Correctness model — that remains future work. It is the minimal
coordination fix needed so a global account switch cannot stall a
worktree that isn't using the shared runtime at all.
@srs-adamr srs-adamr force-pushed the srs-adamr/feat-per-worktree-claude-account branch from d6420c2 to 30f82ca Compare July 1, 2026 11:46
@srs-adamr srs-adamr marked this pull request as ready for review July 2, 2026 00:34
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 033f12e6-4177-4398-94fe-7b6a7d701c2a

📥 Commits

Reviewing files that changed from the base of the PR and between 30b3b50 and 9a51c9e.

📒 Files selected for processing (1)
  • src/main/claude-accounts/runtime-auth-service.test.ts
👮 Files not reviewed due to content moderation or server errors (1)
  • src/main/claude-accounts/runtime-auth-service.test.ts

📝 Walkthrough
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately reflects the main change: per-worktree Claude account binding with concurrent multi-account support.
Description check ✅ Passed All required sections are present, including screenshots, testing, cross-platform review, security audit, and notes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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: 2

🧹 Nitpick comments (1)
src/renderer/src/components/NewWorkspaceComposerCard.test.tsx (1)

119-121: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding coverage for the new Account selector.

Props are wired correctly for the default render, but no test exercises rendering the Select when claudeAccounts is non-empty, or verifies onClaudeAccountIdChange receives null when INHERIT_GLOBAL_CLAUDE_ACCOUNT_VALUE is selected vs. the account id otherwise.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 296a1bb5-1312-4bd5-b8fc-f7526e879ff1

📥 Commits

Reviewing files that changed from the base of the PR and between 0dfd56a and 30f82ca.

📒 Files selected for processing (26)
  • src/main/claude-accounts/runtime-auth-service.test.ts
  • src/main/claude-accounts/runtime-auth-service.ts
  • src/main/claude-accounts/runtime-selection.ts
  • src/main/index.ts
  • src/main/ipc/pty.test.ts
  • src/main/ipc/pty.ts
  • src/main/ipc/worktree-metadata-merge.ts
  • src/main/ipc/worktree-remote.ts
  • src/main/ipc/worktrees.ts
  • src/main/window/attach-main-window-services.ts
  • src/renderer/src/components/NewWorkspaceComposerCard.test.tsx
  • src/renderer/src/components/NewWorkspaceComposerCard.tsx
  • src/renderer/src/components/NewWorkspaceComposerModal.tsx
  • src/renderer/src/components/sidebar/WorktreeContextMenu.tsx
  • src/renderer/src/hooks/useComposerState.ts
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/lib/claude-account-runtime-filter.ts
  • src/renderer/src/lib/pending-worktree-creation.ts
  • src/renderer/src/lib/worktree-creation-flow.ts
  • src/renderer/src/store/slices/worktree-helpers.ts
  • src/renderer/src/store/slices/worktrees.ts
  • src/shared/types.ts

Comment thread src/main/claude-accounts/runtime-auth-service.ts
Comment thread src/renderer/src/components/NewWorkspaceComposerModal.tsx
- runtime-auth-service: memoize the Windows WSL owned-auth-path resolution
  (execFileSync('wsl.exe')) so hasInjectedAccountOverride() no longer blocks
  the main process on a synchronous subprocess in the PTY spawn hot path.
  Only successful resolutions are cached; failures re-attempt.
- NewWorkspaceComposerModal: add .catch() to the claudeAccounts.list() fetch
  so an IPC rejection degrades to an empty list instead of an unhandled
  promise rejection.
- NewWorkspaceComposerCard.test: cover the Account selector — conditional
  render, controlled value display, and that onClaudeAccountIdChange emits the
  account id on select and null for "Inherit global".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@srs-adamr

Copy link
Copy Markdown
Contributor Author

Addressed the automated review feedback in 02de27d:

  • Perf (main-process hot path): hasInjectedAccountOverride() reaches getOwnedManagedAuthPath(), which on Windows validates WSL-pinned accounts via a synchronous execFileSync('wsl.exe', …). Since that check runs on the PTY spawn gate, it's now memoized per account.id:managedAuthPath — only successful resolutions are cached, so a transiently-unavailable distro isn't sticky. (host/macOS/Linux never shelled out; they hit cheap sync fs.)
  • Stability: added .catch() to the claudeAccounts.list() fetch in NewWorkspaceComposerModal so an IPC rejection degrades to an empty list rather than an unhandled promise rejection.
  • Coverage: added tests for the Account selector in NewWorkspaceComposerCard.test.tsx — conditional render, controlled value display, and that onClaudeAccountIdChange emits the account id on select and null for "Inherit global".

typecheck, targeted vitest suites (claude-accounts, pty, composer card — all green), and lint on the touched files all pass locally.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/main/claude-accounts/runtime-auth-service.ts (2)

789-797: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve default-WSL account matching when the target has no distro.

All callers pass the target through resolveWslDefaultTarget() first, so a launch with no distro becomes the concrete default distro. That makes a pinned WSL account stored with wslDistro: null / __default__ fail this comparison and fall back to global selection even though it represents the default WSL account.

🐛 Proposed fix
-    const runtimeMismatch =
+    const targetWslKey = getClaudeWslSelectionKey(normalizedTarget.wslDistro)
+    const defaultWslKey = getClaudeWslSelectionKey(getDefaultWslDistro())
+    const accountWslKey = getClaudeWslSelectionKey(account.wslDistro)
+    const wslDistroMatches =
+      accountWslKey === targetWslKey ||
+      (normalizedTarget.runtime === 'wsl' &&
+        targetWslKey === defaultWslKey &&
+        accountWslKey === getClaudeWslSelectionKey(null))
+    const runtimeMismatch =
       (normalizedTarget.runtime === 'host' && accountIsWsl) ||
-      (normalizedTarget.runtime === 'wsl' &&
-        (!accountIsWsl ||
-          getClaudeWslSelectionKey(account.wslDistro) !==
-            getClaudeWslSelectionKey(normalizedTarget.wslDistro)))
+      (normalizedTarget.runtime === 'wsl' && (!accountIsWsl || !wslDistroMatches))

845-850: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Do not overwrite refreshed injected Keychain credentials with stale managed credentials.

For macOS injected host launches, Claude can refresh the scoped Keychain service for account.managedAuthPath. The next launch always seeds that scoped service from managed storage, but this path never reads the scoped service back first; a rotated refresh token can be replaced with the stale managed copy and break subsequent auth.

Before writing, read the scoped service for account.managedAuthPath and persist/keep it when it matches this account and is fresher or has a non-older rotated refresh token, mirroring the existing read-back logic used for global runtime auth.

🧹 Nitpick comments (1)
src/renderer/src/components/NewWorkspaceComposerCard.test.tsx (1)

180-184: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

as never erases type safety for the fixture.

Casting the fixture as never bypasses structural checks against ClaudeManagedAccountSummary[] entirely, so future required-field additions to that type won't be caught here.

♻️ Suggested safer typing
-const claudeAccounts = [
-  { id: 'acct-alice', email: 'alice@example.com' },
-  { id: 'acct-bob', email: 'bob@example.com' }
-] as never
+const claudeAccounts = [
+  { id: 'acct-alice', email: 'alice@example.com' },
+  { id: 'acct-bob', email: 'bob@example.com' }
+] as unknown as ClaudeManagedAccountSummary[]

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 71715fcb-9f85-447a-b009-3eaac928fafb

📥 Commits

Reviewing files that changed from the base of the PR and between 30f82ca and 02de27d.

📒 Files selected for processing (3)
  • src/main/claude-accounts/runtime-auth-service.ts
  • src/renderer/src/components/NewWorkspaceComposerCard.test.tsx
  • src/renderer/src/components/NewWorkspaceComposerModal.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/renderer/src/components/NewWorkspaceComposerModal.tsx

Comment thread src/renderer/src/components/NewWorkspaceComposerCard.test.tsx
…e-seed

Addresses the second CodeRabbit review pass:

- resolveInjectedAccount: canonicalize the WSL distro key so a default-WSL
  account (wslDistro: null -> '__default__') matches a launch whose distro
  resolved to the concrete default. Also fixes resolveWslDefaultTarget to
  preserve overrideAccountId when it fills in the default distro, so a
  distro-less WSL launch no longer drops its per-worktree pin. (functional
  correctness) + regression test.
- seedInjectedHostAccountKeychain: only bootstrap the config-dir-scoped
  Keychain on first launch. Once Claude owns that scoped service it may hold a
  rotated refresh token (injected accounts are CLI-owned, no read-back), so
  re-seeding the static managed copy would clobber it. Skip when valid creds
  already exist there. (macOS data integrity) + regression test.
- NewWorkspaceComposerCard.test: restore the HTMLElement.prototype pointer/
  scroll polyfills in a finally so they don't leak across tests; type the
  account fixture as ClaudeManagedAccountSummary[] instead of `as never`.

typecheck + full claude-accounts/pty/composer suites (364) green; lint clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@srs-adamr

Copy link
Copy Markdown
Contributor Author

Addressed the second review pass in 30b3b50.

🟠 Default-WSL account matching (functional correctness) — verified real and fixed, plus a related bug found while tracing it:

  • resolveInjectedAccount now canonicalizes the WSL distro key, so a default-WSL account stored with wslDistro: null (__default__) matches a launch whose distro resolved to the concrete default (e.g. Ubuntu). getDefaultWslDistro() is process-lifetime cached and a no-op off Windows, so no hot-path cost.
  • While reproducing it I found resolveWslDefaultTarget was dropping overrideAccountId when it substituted the default distro — so a distro-less WSL launch silently lost its per-worktree pin. Fixed to preserve the other target fields. Regression test added covering both.

🟠 Injected Keychain re-seed clobber (macOS data integrity) — verified real and fixed. On macOS the seed reads the managed keychain (keyed by account.id) and writes the config-dir-scoped service (keyed by sha256(managedAuthPath)). Claude rotates only the scoped service, and injected accounts are CLI-owned (no read-back), so re-seeding the static managed copy on the next launch would clobber a rotated refresh token. seedInjectedHostAccountKeychain now bootstraps only on first launch — if valid creds already exist in the scoped service, Claude's copy is left in place. The scoped service for a per-account config dir only ever holds that account's creds, so an existence guard is sufficient (no full read-back needed). Regression test added.

🟡/🔵 Test hardening — the HTMLElement.prototype pointer/scroll polyfills are now restored in a finally so they can't leak into sibling tests, and the account fixture is typed as ClaudeManagedAccountSummary[] instead of as never.

typecheck + the full claude-accounts / pty / composer suites (364 tests) are green; lint clean on the touched files.

@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.

🧹 Nitpick comments (1)
src/main/claude-accounts/runtime-auth-service.test.ts (1)

3829-3854: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Forced non-null assertion on originalReadImpl risks a silent mock-restore no-op.

originalReadImpl is typed as possibly undefined (from getMockImplementation()), but the finally block restores it with readStrict.mockImplementation(originalReadImpl!). If no prior implementation was set for readActiveClaudeKeychainCredentialsStrict in this suite, this silently swaps the mock's implementation to undefined rather than genuinely restoring prior state — masking the intent and risking subtle isolation drift for later tests, similar to the mock-restore issues already hardened elsewhere in this PR (HTMLElement.prototype polyfills restored in finally).

♻️ Safer restore
     } finally {
-      readStrict.mockImplementation(originalReadImpl!)
+      if (originalReadImpl) {
+        readStrict.mockImplementation(originalReadImpl)
+      } else {
+        readStrict.mockReset()
+      }
     }

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3405120c-aae3-464c-9f36-1b64874798c4

📥 Commits

Reviewing files that changed from the base of the PR and between 02de27d and 30b3b50.

📒 Files selected for processing (3)
  • src/main/claude-accounts/runtime-auth-service.test.ts
  • src/main/claude-accounts/runtime-auth-service.ts
  • src/renderer/src/components/NewWorkspaceComposerCard.test.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/renderer/src/components/NewWorkspaceComposerCard.test.tsx
  • src/main/claude-accounts/runtime-auth-service.ts

…null

Third CodeRabbit pass (trivial): guard the finally-block restore of
readActiveClaudeKeychainCredentialsStrict — restore the captured
implementation when present, else mockReset() — instead of asserting it
non-null, mirroring the polyfill-restore hardening elsewhere in this PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

2 participants