Repository navigation
feat(claude-accounts): per-project Claude account (ask on launch, remember per project) - #23228
Open
Ethan-Rivas wants to merge 119 commits into
Open
Ethan-Rivas wants to merge 119 commits into
Ethan-Rivas wants to merge 119 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.
…t-account # Conflicts: # src/main/persistence/loading-store/repo-lifecycle-operations.ts # src/main/persistence/tracking-repos/repo-update-operations.ts
…t-account # Conflicts: # src/main/ipc/pty/ipc/spawn-commit.ts
…t-account # Conflicts: # src/main/runtime/runtime-folder-worktree-create.ts # src/main/runtime/runtime-local-worktree-terminal-startup.ts # src/main/runtime/workspaces/startup/workspace-terminal-provisioning.ts
…t-account # Conflicts: # src/renderer/src/components/tab-bar/QuickLaunchButton.tsx # src/renderer/src/components/tab-bar/use-tab-bar-create-menu-controller.ts # src/renderer/src/lib/launch-agent-in-new-tab.ts # src/renderer/src/lib/launch-agent-web-host-tab.ts
…t-account # Conflicts: # src/cli/specs/core.ts # src/main/agent-launch/agent-launch-mode.ts
…e executor test harness into their own modules to keep files under the line limit
…t-account # Conflicts: # src/shared/protocol-version.ts # src/shared/runtime-capability-constants.ts
…t-account # Conflicts: # src/renderer/src/components/tab-bar/TabHoverCard.tsx
…t-account # Conflicts: # src/main/ipc/pty/runtime/spawn-commit.ts # src/renderer/src/components/terminal-pane/TerminalErrorToast.tsx
…t-account # Conflicts: # src/main/startup/main-window-agent-status.ts
…t-account # Conflicts: # 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
…t-account # Conflicts: # src/shared/workspace-layout/workspace-layout-disk-fields.ts # src/shared/workspace-layout/workspace-layout-fact-locations.test.ts
…t-account # Conflicts: # 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/shared/account-runtime-capabilities.ts
…t-account # Conflicts: # src/renderer/src/hooks/composer-state/quick-creation-execution.ts # src/renderer/src/lib/agent-session-launch-plan.ts
…t-account # Conflicts: # src/renderer/src/components/settings/RepositoryPane.tsx
…t-account # Conflicts: # src/main/ipc/pty/runtime/spawn-commit.ts
…t-account # Conflicts: # src/main/ipc/pty/ipc/spawn-commit.ts # src/main/ipc/pty/runtime/spawn-commit.ts
…t-account # Conflicts: # src/renderer/src/components/settings/RepositoryPane.tsx
…ew-tab account test
…t-account # Conflicts: # src/renderer/src/components/settings/RepositoryPane.tsx
…t-account # Conflicts: # src/renderer/src/i18n/locales/es.json
…t-account # Conflicts: # src/renderer/src/components/settings/RepositoryPane.tsx
…t-account # Conflicts: # src/main/agent-launch/agent-launch-executor.test.ts # src/main/agent-launch/agent-launch-mode.ts # src/main/claude-accounts/claude-credential-persistence-ratchet.test.ts # src/main/claude-accounts/claude-profile-router.ts
…t-account # Conflicts: # src/main/persistence/tracking-repos/repo-hydration.ts # src/renderer/src/store/repos/repo-update.ts # src/shared/repo-types.ts # src/shared/rpc-contract/repo-update-params.ts
…t-account # Conflicts: # src/renderer/src/components/terminal-pane/TerminalErrorToast.tsx
… its own component to keep the error toast under the line limit
… and keep worker test imports single
…t-account # Conflicts: # src/main/ipc/agent-hooks.test.ts
…t-account # Conflicts: # src/renderer/src/lib/launch-agent-web-host-tab.ts
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)