From 4d70e19e96d4f892aa5fcaec2365da3b9ee8114e Mon Sep 17 00:00:00 2001 From: Merge Sim Date: Mon, 31 Aug 2026 18:06:23 -0700 Subject: [PATCH] Route Claude panes through a per-account CLAUDE_CONFIG_DIR Mirrors the CODEX_HOME spawn-time mechanism: ORCA_CLAUDE_CONFIG_DIR joins the positive overlay allowlist and each shell wrapper re-exports CLAUDE_CONFIG_DIR after user rc files, so a hand-typed claude inherits the selected account. WIP recovered from an interrupted worker; per-account home creation and the shared-store migration are not implemented yet. --- src/main/claude-accounts/environment.ts | 2 +- .../claude-accounts/runtime-auth-service.ts | 10 +++++++++- .../runtime-auth/runtime-auth-preparation.ts | 19 +++++++++++++++++++ .../runtime-auth/runtime-auth-sync.ts | 7 +++++++ .../daemon-zsh-shell-ready-wrapper-spec.ts | 1 + src/main/powershell-osc133-bootstrap.ts | 1 + .../local-pty-shell-ready-bash-rcfile.ts | 2 ++ .../local-pty-shell-ready-wrapper-fileset.ts | 1 + .../local-pty-windows-spawn-environment.ts | 19 ++++++++++++++++++- src/main/shell-startup-features.ts | 1 + src/main/zsh-startup-wrapper-builder.ts | 5 +++++ 11 files changed, 65 insertions(+), 3 deletions(-) diff --git a/src/main/claude-accounts/environment.ts b/src/main/claude-accounts/environment.ts index 83fe3b40209..35dd3c1825a 100644 --- a/src/main/claude-accounts/environment.ts +++ b/src/main/claude-accounts/environment.ts @@ -24,7 +24,7 @@ export function applyClaudeEnvPatch( } } - if (patch.CLAUDE_CONFIG_DIR) { + if (patch.CLAUDE_CONFIG_DIR && !baseEnv.CLAUDE_CONFIG_DIR) { baseEnv.CLAUDE_CONFIG_DIR = patch.CLAUDE_CONFIG_DIR } if (patch.ANTHROPIC_CUSTOM_HEADERS !== undefined) { diff --git a/src/main/claude-accounts/runtime-auth-service.ts b/src/main/claude-accounts/runtime-auth-service.ts index b6fa413b04e..1866cb7e259 100644 --- a/src/main/claude-accounts/runtime-auth-service.ts +++ b/src/main/claude-accounts/runtime-auth-service.ts @@ -19,7 +19,15 @@ export class ClaudeRuntimeAuthService extends ClaudeRuntimeAuthSync { target?: ClaudeAccountSelectionTarget ): Promise { const effectiveTarget = target ?? this.getDefaultAccountSelectionTarget() - await this.syncForCurrentSelection(effectiveTarget) + const settings = this.store.getSettings() + const selectedId = getSelectedClaudeAccountIdForTarget(settings, effectiveTarget) + const selected = selectedId + ? settings.claudeManagedAccounts.find((account) => account.id === selectedId) + : null + // Isolated accounts are already Claude's runtime store; never copy them into ~/.claude. + if (!selected || selected.managedAuthRuntime === 'wsl') { + await this.syncForCurrentSelection(effectiveTarget) + } return this.getPreparation(effectiveTarget) } diff --git a/src/main/claude-accounts/runtime-auth/runtime-auth-preparation.ts b/src/main/claude-accounts/runtime-auth/runtime-auth-preparation.ts index ae79c4c7bbb..2ad0ee9999a 100644 --- a/src/main/claude-accounts/runtime-auth/runtime-auth-preparation.ts +++ b/src/main/claude-accounts/runtime-auth/runtime-auth-preparation.ts @@ -10,6 +10,7 @@ import { } from '../runtime-selection' import { ClaudeRuntimeAuthSnapshotRestore } from './runtime-auth-snapshot-restore' import type { ClaudeRuntimeAuthPreparation } from './runtime-auth-types' +import { resolveOwnedClaudeManagedAuthPath } from '../managed-auth-path' export class ClaudeRuntimeAuthPreparationService extends ClaudeRuntimeAuthSnapshotRestore { protected getPreparation(target?: ClaudeAccountSelectionTarget): ClaudeRuntimeAuthPreparation { @@ -63,6 +64,24 @@ export class ClaudeRuntimeAuthPreparationService extends ClaudeRuntimeAuthSnapsh provenance: `wsl:${normalizeClaudeAccountSelectionTarget(normalizedTarget).wslDistro ?? '__default__'}:system` } } + if (activeAccount?.managedAuthRuntime !== 'wsl' && activeAccount) { + const managedPath = resolveOwnedClaudeManagedAuthPath( + activeAccount.id, + activeAccount.managedAuthPath, + { adoptLegacyMarker: true } + ) + if (managedPath) { + return { + configDir: managedPath, + runtime: 'host', + wslDistro: null, + wslLinuxConfigDir: null, + envPatch: { CLAUDE_CONFIG_DIR: managedPath }, + stripAuthEnv: true, + provenance: `managed:${activeAccount.id}` + } + } + } return { configDir: paths.configDir, runtime: 'host', diff --git a/src/main/claude-accounts/runtime-auth/runtime-auth-sync.ts b/src/main/claude-accounts/runtime-auth/runtime-auth-sync.ts index f191689a446..0f2c7f5ac02 100644 --- a/src/main/claude-accounts/runtime-auth/runtime-auth-sync.ts +++ b/src/main/claude-accounts/runtime-auth/runtime-auth-sync.ts @@ -23,6 +23,13 @@ export class ClaudeRuntimeAuthSync extends ClaudeRuntimeAuthPreparationService { this.lastSyncedAccountId ) this.managedRefreshDeferredByLivePtyAccountId = null + // Per-account Claude config dirs are self-contained; syncing them into the + // shared ~/.claude would recreate the stale-sibling race this isolation removes. + if (activeAccount?.managedAuthRuntime === 'host') { + this.clearLastWrittenRuntimeState() + this.lastSyncedAccountId = activeAccount.id + return + } const previousManagedCredentialsJson = previousAccount ? await this.readManagedCredentials(previousAccount) : null diff --git a/src/main/daemon/daemon-zsh-shell-ready-wrapper-spec.ts b/src/main/daemon/daemon-zsh-shell-ready-wrapper-spec.ts index 70ea670966c..3f0fa53216c 100644 --- a/src/main/daemon/daemon-zsh-shell-ready-wrapper-spec.ts +++ b/src/main/daemon/daemon-zsh-shell-ready-wrapper-spec.ts @@ -14,6 +14,7 @@ export function getDaemonZshWrapperSpec(): ZshStartupHookSpec { agentTeamsPath: true, remoteCliBinDir: false, codexHome: true, + claudeConfigDir: true, codexLaunchPreflight: true } } diff --git a/src/main/powershell-osc133-bootstrap.ts b/src/main/powershell-osc133-bootstrap.ts index 1f356b1c332..e05600d6e6b 100644 --- a/src/main/powershell-osc133-bootstrap.ts +++ b/src/main/powershell-osc133-bootstrap.ts @@ -8,6 +8,7 @@ const POWERSHELL_OSC133_BOOTSTRAP = `# Orca OSC 133 shell integration for PowerS if ($env:ORCA_OPENCODE_CONFIG_DIR) { $env:OPENCODE_CONFIG_DIR = $env:ORCA_OPENCODE_CONFIG_DIR } if ($env:ORCA_MIMOCODE_HOME) { $env:MIMOCODE_HOME = $env:ORCA_MIMOCODE_HOME } if ($env:ORCA_CODEX_HOME) { $env:CODEX_HOME = $env:ORCA_CODEX_HOME } +if ($env:ORCA_CLAUDE_CONFIG_DIR) { $env:CLAUDE_CONFIG_DIR = $env:ORCA_CLAUDE_CONFIG_DIR } if ($ExecutionContext.SessionState.LanguageMode -eq "FullLanguage" -and ((-not (Test-Path variable:global:__OrcaOsc133State)) -or diff --git a/src/main/providers/local-pty-shell-ready-bash-rcfile.ts b/src/main/providers/local-pty-shell-ready-bash-rcfile.ts index f752b75f67a..65bb414f340 100644 --- a/src/main/providers/local-pty-shell-ready-bash-rcfile.ts +++ b/src/main/providers/local-pty-shell-ready-bash-rcfile.ts @@ -52,6 +52,8 @@ __orca_restore_agent_teams_path ${getPosixOmpShellWrapper()} # Why: Codex must keep using Orca's runtime CODEX_HOME after profile scripts. [[ -n "\${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="\${ORCA_CODEX_HOME}" +# Why: Claude must keep using Orca's runtime config directory after profile scripts. +[[ -n "\${ORCA_CLAUDE_CONFIG_DIR:-}" ]] && export CLAUDE_CONFIG_DIR="\${ORCA_CLAUDE_CONFIG_DIR}" ${getPosixCodexShellLaunchPreflight()} # Why: emit OSC 133 C/D so terminal-command-lifecycle can drop stale agent # status when the foreground command (e.g. an interrupted Claude/Codex CLI) diff --git a/src/main/providers/local-pty-shell-ready-wrapper-fileset.ts b/src/main/providers/local-pty-shell-ready-wrapper-fileset.ts index f0e3f792fc0..b995a5295af 100644 --- a/src/main/providers/local-pty-shell-ready-wrapper-fileset.ts +++ b/src/main/providers/local-pty-shell-ready-wrapper-fileset.ts @@ -27,6 +27,7 @@ export function getLocalZshWrapperSpec(): ZshStartupHookSpec { agentTeamsPath: true, remoteCliBinDir: false, codexHome: true, + claudeConfigDir: true, codexLaunchPreflight: true } } diff --git a/src/main/providers/local-pty-windows-spawn-environment.ts b/src/main/providers/local-pty-windows-spawn-environment.ts index f7967d710f7..5352e9ddea2 100644 --- a/src/main/providers/local-pty-windows-spawn-environment.ts +++ b/src/main/providers/local-pty-windows-spawn-environment.ts @@ -53,7 +53,20 @@ export function finalizeWindowsLocalPtySpawnEnvironment(args: { } if (env.CLAUDE_CONFIG_DIR) { // Why: managed WSL Claude passes a Linux CLAUDE_CONFIG_DIR through wsl.exe; non-default vars need WSLENV import. - addWslEnvKeys(env, ['CLAUDE_CONFIG_DIR']) + const claudeConfigWslInfo = parseWslPath(env.CLAUDE_CONFIG_DIR) + if (claudeConfigWslInfo) { + if (plan.launchWslDistro && plan.launchWslDistro !== claudeConfigWslInfo.distro) { + delete env.CLAUDE_CONFIG_DIR + delete env.ORCA_CLAUDE_CONFIG_DIR + } else { + env.CLAUDE_CONFIG_DIR = claudeConfigWslInfo.linuxPath + env.ORCA_CLAUDE_CONFIG_DIR = claudeConfigWslInfo.linuxPath + addWslEnvKeys(env, ['CLAUDE_CONFIG_DIR', 'ORCA_CLAUDE_CONFIG_DIR']) + } + } else { + env.ORCA_CLAUDE_CONFIG_DIR ??= env.CLAUDE_CONFIG_DIR + addWslEnvKeys(env, ['CLAUDE_CONFIG_DIR', 'ORCA_CLAUDE_CONFIG_DIR']) + } } if (env[ORCA_HERMES_STARTUP_QUERY_ENV] !== undefined) { // Why: wsl.exe drops custom Windows env vars; the startup wrapper needs this imported inside WSL. @@ -65,6 +78,10 @@ export function finalizeWindowsLocalPtySpawnEnvironment(args: { delete env.ORCA_CODEX_HOME } + if (pathWin32.basename(plan.shellPath).toLowerCase() !== 'wsl.exe' && env.CLAUDE_CONFIG_DIR) { + env.ORCA_CLAUDE_CONFIG_DIR ??= env.CLAUDE_CONFIG_DIR + } + const shellBasename = pathWin32.basename(plan.shellPath).toLowerCase() const codexLaunchPreflightCommand = env.ORCA_CODEX_LAUNCH_PREFLIGHT if ( diff --git a/src/main/shell-startup-features.ts b/src/main/shell-startup-features.ts index 95518967299..0ba8bfee19d 100644 --- a/src/main/shell-startup-features.ts +++ b/src/main/shell-startup-features.ts @@ -30,6 +30,7 @@ const OVERLAY_ENV_KEYS = [ 'ORCA_MIMOCODE_HOME', 'ORCA_OMP_STATUS_EXTENSION', 'ORCA_CODEX_HOME', + 'ORCA_CLAUDE_CONFIG_DIR', 'ORCA_AGENT_TEAMS_SHIM_DIR', 'ORCA_REMOTE_CLI_BIN_DIR' ] as const diff --git a/src/main/zsh-startup-wrapper-builder.ts b/src/main/zsh-startup-wrapper-builder.ts index 27a1e0b7615..200e1460a54 100644 --- a/src/main/zsh-startup-wrapper-builder.ts +++ b/src/main/zsh-startup-wrapper-builder.ts @@ -46,6 +46,8 @@ export type ZshWrapperRestoreSpec = { codexHome: boolean /** The `codex()` wrapper that runs Orca's launch preflight. */ codexLaunchPreflight: boolean + /** Orca's runtime Claude config directory. */ + claudeConfigDir: boolean } export type ZshStartupHookSpec = { @@ -75,6 +77,8 @@ const MIMOCODE_HOME_RESTORE = `[[ -n "\${ORCA_MIMOCODE_HOME:-}" ]] && export MIM const REMOTE_CLI_BIN_DIR_RESTORE = `[[ -n "\${ORCA_REMOTE_CLI_BIN_DIR:-}" ]] && case ":$PATH:" in *:"\${ORCA_REMOTE_CLI_BIN_DIR}":*) ;; *) export PATH="\${ORCA_REMOTE_CLI_BIN_DIR}:$PATH" ;; esac` const CODEX_HOME_RESTORE = `# Why: Codex must keep using Orca's runtime CODEX_HOME after rc files. [[ -n "\${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="\${ORCA_CODEX_HOME}"` +const CLAUDE_CONFIG_DIR_RESTORE = `# Why: Claude must keep using Orca's runtime config directory after rc files. +[[ -n "\${ORCA_CLAUDE_CONFIG_DIR:-}" ]] && export CLAUDE_CONFIG_DIR="\${ORCA_CLAUDE_CONFIG_DIR}"` /** * The OSC 133 hooks, defined at top level so their bodies are parsed before the @@ -125,6 +129,7 @@ function getOverlayRestoreBlocks(spec: ZshStartupHookSpec): (string | null)[] { spec.restores.remoteCliBinDir ? REMOTE_CLI_BIN_DIR_RESTORE : null, getPosixOmpShellWrapper(), spec.restores.codexHome ? CODEX_HOME_RESTORE : null, + spec.restores.claudeConfigDir ? CLAUDE_CONFIG_DIR_RESTORE : null, spec.restores.codexLaunchPreflight ? getPosixCodexShellLaunchPreflight() : null ] }