Repository navigation
feat(claude-accounts): per-project Claude account (ask on launch, remember per project) - #23228
Ethan-Rivas wants to merge 75 commits into
Conversation
…val and Orca refreshes
…store pinned PTYs at startup
Selection and removal checked for pinned terminals before the pinned launch's reservation existed, and a usage fetch could stage or refresh an account while a pinned launch seeded the same Keychain item. Host mutations, usage fetches and pinned reservations now claim an account with one synchronous check-and-set and hold it until they finish; a pinned launch waits out a usage fetch and refuses a switch or removal in progress.
…queue A pinned launch that met an in-flight usage fetch waited up to 40s inside the process-wide auth mutation queue, stalling every Claude launch and sync. The reservation is now taken (and waited for) before entering the queue; the queued part re-validates the account and the reservation is released on every failure.
… writes Re-auth replaced an account's managed credentials without the host-mutation claim, so it could overwrite the store a pinned --account Claude refreshes. It now refuses up front while pinned terminals hold the account and claims the account from the credential write through rollback.
…als still run on A host Claude keeps the account it started on and refreshes it through ~/.claude after the host switches away, so pinning that account with --account gave one single-use refresh chain two owners. Unpinned host Claude PTYs and structured children now record the account they started on (from the prepared auth provenance, else the host selection), restored across restarts, and a pinned reservation refuses while any of them is still live.
Store an optional Claude account choice on each repo, next to the GitHub account: the default (active) account, a saved managed account, or "ask every time". It is normalized on load and update, and travels over repo.update as a new optional field.
Add an optional claudeAccountId to the sleeping-agent launch config so a launch can record which Claude account it ran on. Recovery equivalence and sleeping records keep the field; nothing reads it yet.
Move the pinned-account preparation, reservation release and spawn registration out of the runtime spawn lane into claude-pinned-spawn so the IPC lane can reuse them. No behavior change.
Pinned launch refusals now end with a [claude_pinned:<code>] token. Errors cross IPC as plain strings, so the token lets the renderer map a refusal to localized copy without parsing the English message.
…anes Resolve the account a fresh Claude launch must run on (explicit --account, then the launch config, then the project's saved account) and pin it on both the renderer IPC lane and the runtime lane. SSH and pre-adopted panes never pin, and host-side agent.launch falls back to a terminal instead of a structured session when the project pins one.
New-tab and New Workspace launches resolve the project's saved account in the renderer and record it in the launch config. A launch that pins an account is routed to a terminal, since a structured session has no way to run on a pinned account.
Add a Claude account picker to repository settings (also for folder projects) and an opt-in "ask per project" toggle under Accounts, both reachable from settings search. The repository project sections move into their own component to keep RepositoryPane under the size limit.
When the ask setting is on and a project has no saved account, Claude launches from the tab bar, quick launch and New Workspace first show a prompt to pick an account, with an option to remember the choice for the project.
…tive account Map pinned-launch refusal codes to localized copy in the terminal error toast, naming the account and terminal count when an account is still in use. Offer "Start on active account" to restart only the refused pane, or Retry for refusals that clear on their own. CLI and mobile error text strips the refusal tag; --json output keeps it.
A pinned account cannot run inside WSL yet. Tag that refusal with a new unsupported-host code so the terminal toast explains it and offers the active account, and disable the Settings picker and the account prompt for WSL projects.
Main reads each pinned PTY's account from the pinned registry and adds it to the agent-status push, so the tab can show which account a Claude terminal runs on. The live-entry history helpers move to their own file to keep the builder under the size limit.
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
This run reviewed the single commit that landed since the prior pullfrog review (50941704): an origin/main merge (0f88550, main at 564f4d0). Its only conflict was src/shared/global-settings-types.ts, resolved as a clean union plus one type extraction.
- Unioned the settings type — the branch's
askClaudeAccountPerProjectnow sits alongside main's newagentStateRulesPath/agentStateRulesLiveUpdates, withagentWorkspaceTrustEnabledretained. Neither side's field was dropped. - Extracted
WorktreeVisibilityDefaultsinto a newsrc/shared/worktree-visibility-defaults.tsand re-exported it fromglobal-settings-types.ts; the union had pushed that file past the.ts300-linemax-linescap. Type-only, and every existing importer still resolves through the original entry point.
Verified two ways: git diff 564f4d0 HEAD over the merge shows only the branch's field and the extraction, and git diff 5094170 HEAD shows only main's fields — no feature line lost, no main change dropped. The merge touched none of the branch's feature files. pnpm tc:node is clean.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
…t-account # Conflicts: # src/main/runtime/rpc/methods/orchestration/worker/local-worker-start.ts
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
This run reviewed what landed since the prior pullfrog review (0f88550): a single origin/main merge (0fab477, main at e80b550, ~50 main commits) plus one merge-only refactor. tsc -p config/tsconfig.node.json is clean, oxlint is clean on all three touched files, and the 25 tests over the merge-touched runtime paths pass.
- Merged
origin/main(0fab477), resolving one textual conflict plus two files that differ from both parents:local-worker-start.ts— union of main's newresolveWorkerConfiguredAgentParams(configured alias → TUI agent preflight, withlaunchParamsfed toprepareLocalWorkerStart) and the branch'sclaudeAccountdestructure, itsassertClaudeAccountWorktreeIsLocalguard, and theclaudeAccountIdpassed toplaceWorkerAgent.orca-runtime-create-managed-worktree.ts+ newruntime-worktree-create-requested-agent.ts— a merge-only extraction of the inline requested-agent resolution intoresolveRequestedStartupAgent(args, disabledTuiAgents), moved verbatim. Main'strackRuntimeWorkspaceCreate/events/timingand the branch'sstartupClaudeAccountIdboth survive.
The merge holds under a two-way check: git diff e80b550 HEAD over the touched files contains only the branch's additions, and git diff 0f88550 HEAD contains only main's, so no branch line was lost and no main change was dropped.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
…t-account # Conflicts: # src/shared/global-settings-types.ts
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
This run reviewed what landed since the prior pullfrog review (0fab477): a single origin/main merge whose only textual conflict was global-settings-types.ts. git diff-tree --cc shows the full both-parents set is seven files; every resolution is a clean union that preserves the branch's feature. pnpm tc:node, tc:web, and tc:cli are clean, oxlint reports zero findings on all seven files, and the affected settings tests pass (18).
- Merged
origin/main(9432b51, main tipb032867), resolving one conflict plus a merge-only refactor:accounts-claude-search.ts(new) — main still hadgetAccountsClaudeSearchEntriesinline inaccounts-search.ts; the branch added the ask-per-project entry and its keyword accessor. Merging main's additional provider entry groups would push the file past the 300-line cap, so the Claude entries were extracted here — main'sgetAccountsClaudeSearchEntrieswith the branch's ask entry appended, plusgetAskClaudeAccountPerProjectSearchKeywords.accounts-search.ts/AccountsPane.tsx— now import the Claude entries and the keyword accessor from the new module; the pane roll-up still includes the Claude entry exactly once, and no consumer still imports either symbol from./accounts-search.accounts-search.test.ts— main's new pane-order test and the branch's ask-per-project test both survive, with imports repointed.global-settings-types.ts— type-only: adopted main'sOrcaWorkspaceLayoutre-export (./orca-workspace-layout) alongside the branch'sWorktreeVisibilityDefaultsre-export.protocol-version.ts— main'sANTIGRAVITY_ACCOUNTS_RUNTIME_CAPABILITYand...SKILL_RUNTIME_CAPABILITIESsit alongside the branch'sCLAUDE_LAUNCH_ACCOUNT_RUNTIME_CAPABILITYandAGENT_SESSION_CLAUDE_ACCOUNT_RUNTIME_CAPABILITY, both retained.
The merge holds under a two-way check: git diff b032867 HEAD over the touched files contains only the branch's additions, and git diff 0fab477 HEAD contains only main's, so no branch line was lost and no main change was dropped.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
…t-account # Conflicts: # src/cli/specs/core.ts # src/cli/specs/orchestration-worker-specs.ts # src/main/agent-launch/agent-launch-mode.ts # src/renderer/src/components/settings/RepositoryPane.tsx # src/renderer/src/components/terminal-pane/TerminalErrorToast.tsx # src/renderer/src/lib/agent-session-launch-plan.ts # src/renderer/src/lib/launch-agent-in-new-tab.ts
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
This run reviewed the single commit since the prior pullfrog review (9432b51): an origin/main merge (0aac4c7, main tip 62451920ed; ~30 main commits including #24205 structured chats on the paired server, #24945 SSH review context, #24998/#24997 jcode, #24923 git catalog reuse, bde1c09866 external worktree visibility, 2fc517c1c6 worktree set --unread/--read). Seven conflicts all resolved as clean unions, and pnpm tc:node, tc:web, and tc:cli are clean.
- Merged
origin/mainand resolved all seven conflicts as unions, verified two ways (git diff 62451920ed 0aac4c7over the conflict files contains only branch additions, so no main line was dropped;git diff 9432b51 0aac4c7contains only main's changes, so no branch line was lost):agent-launch-mode.ts— main'splacement.onearly return (remote_execution_host) andclient-capabilityblocker sit alongside the branch'splacement.claudeAccountearly return and its host-sidedowngradeAgentLaunchModeForProjectClaudeAccount. The merge keepsexecutionHostId: 'local'because theonearly return supersedes main'sruntime:${on}support-probe path.agent-session-launch-plan.ts— main'sexecutionHostId/parseExecutionHostIdverdict field and owner-unresolved catch-toast sit alongside the branch'slaunchPinsClaudeAccount/resolvePlannedRoute;buildAgentLaunchRouteInputis built once and shared.launch-agent-in-new-tab.ts— main's structured-route-first block (launchStructuredAgentFromNewTabwith itsopenTerminalrecursion, before the web-runtime check) sits alongside the branch'sstampClaudeLaunchAccountandclaudeAccountIdthreading.TerminalErrorToast.tsx— main'shumanizeFolderWorkspacePathErrorsand conditional issue link (isFolderWorkspacePathOnlyError) sit alongside the branch'sdescribeClaudePinnedLaunchErrorshort-circuit.RepositoryPane.tsx— main'sRepositoryRemoveProjectButtonextraction andsettingsSelectionKey/settingsEntryRepoIdsthreading sit alongside the branch'sRepositoryClaudeAccountSectionandRepositoryProjectSettingsSections.core.ts/orchestration-worker-specs.ts— main'sworktree-set.ts/repo.tsspec extractions and OMP note sit alongside the branch's--accountflag/note and itsworktree-create.tsextraction.
Verified: no conflict markers anywhere in src/; the src/main/claude-accounts suite (283 tests), the renderer routing/plan/launch and conflict-file suites (147 + 69 tests) are green. check:code-quality:changed reports zero design-system, focused-plugin, and type-aware findings; its 14 casting/React-Doctor findings are all pre-existing branch test-file casts (for example #22691's pinned-launch tests) untouched by this merge.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
…t-account # Conflicts: # src/cli/specs/orchestration-worker-specs.ts # src/main/agent-launch/agent-launch-executor.test.ts # src/main/runtime/orca-runtime-create-managed-worktree.ts # src/main/runtime/rpc/methods/orchestration/worker/local-worker-start.ts # src/main/runtime/rpc/methods/orchestration/worker/worker-start-validation.ts # src/renderer/src/components/tab-bar/QuickLaunchButton.tsx # src/renderer/src/components/tab-bar/use-tab-bar-create-menu-controller.ts # src/renderer/src/components/use-terminal-create-actions.ts # src/renderer/src/hooks/composer-state/quick-creation-execution.ts # src/renderer/src/lib/agent-session-launch-plan.ts # src/renderer/src/lib/launch-agent-in-new-tab.ts
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
This run reviewed the single commit since the prior pullfrog review (0aac4c7): an origin/main merge (2c094bc, main tip d518d4a546). Eleven conflicts were all resolved as clean unions, and a git merge-tree comparison confirms no other file diverges from the automatic 3-way merge. pnpm tc:node, tc:web, and tc:cli are clean; the focused conflict-file tests (80) and the claude-accounts + pty suites (1267 pass / 6 skipped) are green.
- Merged
origin/mainand resolved all eleven conflicts as unions, verified two ways (git diff d518d4a546 2c094bcover the conflict files contains only the branch's additions, so no main line was dropped;git diff 0aac4c7 2c094bccontains only main's changes, so no branch line was lost):orchestration-worker-specs.ts— main's OpenCode--modelnote rides alongside the branch's--accountflag and note.agent-launch-executor.test.ts— the branch's saved-account describe block sits next to main'slineCarriesPrompt/agent/freshLaunchprompt-transport tests.orca-runtime-create-managed-worktree.ts— the branch'sresolveRequestedStartupAgentandstartupClaudeAccountIdsit alongside main'sresolveWorktreeCreateAgentStartupandassertOpenCodeModelLaunchPreferencesAbsent.local-worker-start.ts— the branch'sassertClaudeAccountWorktreeIsLocal/claudeAccountsit alongside main's OpenCode probe andwaitForWorkerAgentReady.worker-start-validation.ts— the branch'swithWorkerLaunchAccount/--accountvalidation sit alongside main'sopenCodeModelLaunchSupportedthreading.QuickLaunchButton.tsx— the branch'slaunchWithClaudeAccountChoicewrapper and deferred focus sit alongside main'sonPromptHandedOff/disabled/newAgentPromptOutcome(main'sonPromptHandedOffcall survives).use-tab-bar-create-menu-controller.ts,use-terminal-create-actions.ts,quick-creation-execution.ts,agent-session-launch-plan.ts,launch-agent-in-new-tab.ts— the branch's account routing andrequestIdadditions unioned with main'sresolveAgentSessionLaunchRoute/hookPreparation/targetGroupId.
- Merge-only edits (5) — all benign: an
oxfmtline-wrap inuse-native-chat-draft.ts; the extraction offocusNewActiveTerminalWhenReadyinto a newnew-tab-menu-terminal-focus.ts(keeps the controller under the 300-line cap at 290); andTerminalPaneSurface.tsx,agent-session-launch-plan.test.ts, andlaunch-agent-in-new-tab-claude-account.test.tseach adding main's now-requiredrequestIdto the branch's existing launch call sites.
Verified: no conflict markers in src/; all seven locale JSONs plus en-runtime-required.json parse with the ClaudePinnedLaunch block intact. All prior pullfrog threads were already [RESOLVED].
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
…t-account # Conflicts: # src/renderer/src/components/tab-bar/SortableTab.tsx
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
This run reviewed the single commit since the prior pullfrog review (2c094bc): an origin/main merge (8862679, main tip 10bea3a8fa) whose only conflict was in the renderer tab bar. git merge-tree confirms it is the sole content conflict, and diffing the result against the reconstructed auto-merge tree shows no other manual edit.
- Merged
origin/main(8862679), resolving the one conflict insrc/renderer/src/components/tab-bar/SortableTab.tsxby adopting main's sharedCLOSE_ALL_CONTEXT_MENUS_EVENTimport from the new@/lib/close-all-context-menusmodule and dropping the branch's localexport const, while keeping the branch'suseClaudeTabAccountLabeltooltip suffix andonTooltipOpenChangewiring. No consumer imported the constant from./SortableTab, so dropping the local export is safe, and main's extraction is a pure consolidation (same event string). - Verified the merge is otherwise mechanical —
git diff <auto-merge-tree> 8862679is limited to the three conflictedSortableTab.tsxlines, so no main change was dropped and no branch line was lost.
Verified: pnpm tc:web clean; the SortableTab render, tab-context-menu consistency, and claude-tab-account-label suites pass (20 tests).
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
…t-account # Conflicts: # src/renderer/src/components/native-chat/use-native-chat-draft.ts
…t-account # Conflicts: # src/shared/global-settings-types.ts
…t-account # Conflicts: # src/shared/protocol-version.ts
…t-account # Conflicts: # src/renderer/src/lib/launch-agent-in-new-tab.ts
…t-account # Conflicts: # src/renderer/src/components/terminal-pane/use-terminal-pane-title-state.ts
…t-account # Conflicts: # src/main/runtime/rpc/methods/orchestration/worker/local-worker-start.ts # src/main/runtime/rpc/methods/orchestration/worker/workers.ts
…t-account # Conflicts: # src/cli/handlers/worktree.ts
…t-account # Conflicts: # src/main/claude-accounts/runtime-auth-service.ts # src/main/ipc/pty/ipc/spawn-preflight.ts # src/main/ipc/pty/runtime/spawn-preflight.ts
…t-account # Conflicts: # src/main/agent-launch/agent-launch-mode.ts # src/renderer/src/lib/agent-session-launch-plan.ts
…t-account # Conflicts: # src/main/claude-accounts/runtime-auth-service.ts
…t-account # Conflicts: # src/cli/handlers/worktree.ts
…t-account # Conflicts: # src/main/runtime/orca-runtime-get-worktree-terminal-provisioning-host.ts
…t-account # Conflicts: # src/renderer/src/components/tab-bar/QuickLaunchButton.tsx # src/renderer/src/lib/launch-agent-in-new-tab.ts
…t-account # Conflicts: # src/renderer/src/components/native-chat/NativeChatComposer.tsx
…t-account # Conflicts: # docs/reference/agent-status-store.md
…t-account # Conflicts: # src/renderer/src/lib/agent-session-launch-plan.ts # src/renderer/src/lib/launch-agent-in-new-tab.ts
…t-account Rebuilds pinned Claude launches on main's per-account folders (stablyai#24434, Step 4), which removed credential replay. A `--account` (or project-saved) launch on an account that is not the selected one now runs Claude straight from that account's own folder: CLAUDE_CONFIG_DIR points at it and the pane gets no which-account pointer, so a later switch of the selected account never moves it. The selected account still takes main's normal path. Because no login is copied anywhere any more, the credential seeding, Keychain read-back, per-account reservations, usage-fetch and account-switch guards, and the host-terminal account tracking are removed. The pinned PTY registry keeps only which PTY runs which account, for the tab and status labels, and the refusal codes those guards produced (and the toast's Retry) are pruned.
…t-account # Conflicts: # src/shared/protocol-version.ts

ELI5
If you use Claude with more than one account (say work and personal), Orca today has one "active" account for everything, so you have to remember to switch before working in each project, and switching affects every project at once. This PR lets each project remember its Claude account. Optionally, Orca asks the first time you start Claude in a project and remembers your choice, so work repos always start on the work account and personal repos on the personal one, even side by side. If the saved account can't be used, Claude doesn't quietly start on a different account; the pane explains why and lets you start on the active account for that one launch.
What Changed
Before: one global Claude account per machine. Every Claude tab, workspace and resume used whichever account was selected when it started. #22691 adds
--accountfor the CLI and RPC only; the desktop app's launches (new tab, ⌘⌥T, New Workspace, resume) can't use it.After, for the user:
Default·Ask every time· each signed-in Claude account. Disabled for SSH and WSL projects, with a note.Mechanism:
Repo.agentAccounts.claude = { mode: 'ask' } | { mode: 'account', accountId }, round-tripped likeghAccount(feat(github): bind projects to a specific gh account #13664): persistence, IPC, therepo.updateschema (nullclears).SleepingAgentLaunchConfig.claudeAccountIdcarries a launch's account (or an "active this time" sentinel), so resume and restore replay it.prepareForClaudeLaunch(target, { accountId })sets up that account's folder (claude-profiles/<id>/home, via the router's newprepareAccountLaunch) and pointsCLAUDE_CONFIG_DIRat it. The pane gets no which-account pointer, so main'sclaudeshell function isn't defined there and a later switch can't redirect it. The selected account takes main's normal path. Because nothing is copied, there are no reservations, Keychain seeding, read-back or switch guards.[claude_pinned:<code>]token (account-missing,provenance,unsupported-host) so the renderer can localize them; the CLI and mobile strip it from human output (--jsonkeeps it).claudeAccountIdonto agent-status rows from it, and the tab label reads that.terminal.createAgentSession/ensureAgentSessioncarry the account only to hosts advertising the newagent-session.claude-account.v1capability (their params are strict, so older hosts would otherwise reject the launch).pinned_claude_accountreason), and a structured → terminal handoff keeps the session's own account. A pinned launch also skips the new-tab wait for a host's agent list, since it is a terminal whatever the host answers.Why
Defaultkeeps today's behavior, so users who don't use it see no change.Linked Issue
Fixes #23227
Visual Proof
Settings → Repository has no Claude account choice
New "Claude Account" section: Default / Ask every time / each account
"+ → Claude" starts on the global account with no choice
With "ask" on, a prompt picks the account (Remember for this project)
Tab tooltip shows no account
A pinned tab shows its account email
If the saved account can't be used: explanation + Start on active account
The refused pane restarted in place on the active account
Testing
I manually tested these changes locally
Automated tests added/updated
Unit and integration tests:
e2e:
tests/e2e/per-project-claude-account.spec.tsruns the real app hidden, with fake managed accounts and a stand-inclaude. It checks the settings options, the prompt, Remember, that the pinnedCLAUDE_CONFIG_DIRis the account's own folder (set up on first use), no re-prompt plus the tab label, and a refusal restarting in place on the active account. It runs on Linux CI and skips on macOS and Windows. Screenshots are written only whenORCA_PER_PROJECT_CLAUDE_ACCOUNT_SCREENSHOT_DIRis set.Manual: to re-run on the account-folder rebuild (the earlier manual runs were on the credential-copying design): two projects on two accounts at once with
/statusconfirming each, a one-time pick versus Remember, the prompt over New Workspace and Cancel creating nothing, a refusal and in-place restart, switching the selected account while a pinned tab runs, and an app restart bringing pinned tabs back on their accounts with labels.Platforms: macOS (manual, to re-run). Linux runs in CI (e2e). Windows: unit tests only; the e2e skips there (the stand-in
claudeis POSIX). WSL projects are deliberately not pinned. SSH projects are never pinned (covered by tests).pnpm tcpasses.pnpm testover the account, terminal, launch, CLI and worker suites: every failure also fails on a cleanmainin the same environment.AI Disclosure
Built with Claude Code (Claude Opus 5.5, with Sonnet 5 / Haiku 4.5 sub-agents for smaller tasks): design, implementation, tests and reviews, including the rebuild on main's account folders. Every change was reviewed and tested by me.
Review
AI review summary:
agentAccounts.claude) for later agents. Non-Claude terminals take the same code path as before.Dialog/Select/Checkbox/Switch; strings throughtranslatein all 5 locales; design-system lint clean on changed lines.Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes
Known limitations
Related: #22893 (opt-in isolation from external agent config) is complementary: pinned sessions run in their account's own folder and never write to
~/.claude.Follow-ups: WSL pinning on main's WSL account folders, per-worktree overrides, project-group inheritance (#6921 discussion), per-account usage bars, and Codex once it has a per-launch account primitive.
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred) (the fullpnpm lintandpnpm buildweren't run locally: dependencies main added since can't be installed in this environment; CI will cover them)