diff --git a/src/utils/shell-executor.ts b/src/utils/shell-executor.ts index b6f26ab2..3764f484 100644 --- a/src/utils/shell-executor.ts +++ b/src/utils/shell-executor.ts @@ -26,6 +26,28 @@ export function stripAnthropicEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv { return result; } +const ANTHROPIC_ROUTING_ENV_KEYS = new Set([ + 'ANTHROPIC_BASE_URL', + 'ANTHROPIC_AUTH_TOKEN', + 'ANTHROPIC_API_KEY', +]); + +/** + * Strip inherited Anthropic routing/auth env while preserving model intent. + * Used for nested settings-profile Claude launches where `--settings` already + * defines the provider transport and the parent process should only lend model + * defaults or effort hints. + */ +export function stripAnthropicRoutingEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv { + const result: NodeJS.ProcessEnv = {}; + for (const key of Object.keys(env)) { + if (!ANTHROPIC_ROUTING_ENV_KEYS.has(key)) { + result[key] = env[key]; + } + } + return result; +} + /** * Strip Claude Code nested-session guard env var from a process environment. * @@ -137,14 +159,18 @@ export function execClaude( const webSearchEnv = getWebSearchHookEnv(); const claudeLaunchEnv = getClaudeLaunchEnvOverrides(); - // For account/default profiles, strip ANTHROPIC_* from parent env to prevent - // stale proxy config (e.g., from prior CLIProxy sessions) from interfering - // with native Claude API routing. Settings-based profiles explicitly inject - // their own ANTHROPIC_* values, so they don't need this protection. + // Strip inherited ANTHROPIC_* when the launch should not reuse parent routing. + // Account/default profiles need full isolation from prior proxy sessions. + // Settings profiles can selectively strip only routing/auth when `--settings` + // already carries the provider source of truth but the parent model intent + // should still flow into nested Team/subagent launches. const profileType = envVars?.CCS_PROFILE_TYPE; - const baseEnv = - profileType === 'account' || profileType === 'default' - ? stripAnthropicEnv(process.env) + const stripInheritedAnthropicEnv = profileType === 'account' || profileType === 'default'; + const stripInheritedAnthropicRoutingEnv = envVars?.CCS_STRIP_INHERITED_ANTHROPIC_ENV === '1'; + const baseEnv = stripInheritedAnthropicEnv + ? stripAnthropicEnv(process.env) + : stripInheritedAnthropicRoutingEnv + ? stripAnthropicRoutingEnv(process.env) : process.env; // Prepare environment (merge with base env if envVars provided) diff --git a/tests/unit/utils/claudecode-env-stripping.test.ts b/tests/unit/utils/claudecode-env-stripping.test.ts index 5fb99ded..96ca54df 100644 --- a/tests/unit/utils/claudecode-env-stripping.test.ts +++ b/tests/unit/utils/claudecode-env-stripping.test.ts @@ -336,6 +336,37 @@ describe('CLAUDECODE environment stripping', () => { expect(normalizeSpy).toHaveBeenCalledWith(instancePath); }); + it('execClaude strips inherited ANTHROPIC routing env but keeps model intent for settings-profile Claude launches', () => { + process.env.ANTHROPIC_BASE_URL = 'http://127.0.0.1:8317/api/provider/codex'; + process.env.ANTHROPIC_AUTH_TOKEN = 'ccs-internal-managed'; + process.env.ANTHROPIC_API_KEY = 'stale-api-key'; + process.env.ANTHROPIC_MODEL = 'gpt-5.4'; + process.env.ANTHROPIC_DEFAULT_OPUS_MODEL = 'gpt-5.4'; + process.env.ANTHROPIC_DEFAULT_SONNET_MODEL = 'gpt-5.4'; + process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = 'gpt-5.4-mini'; + process.env.ANTHROPIC_SMALL_FAST_MODEL = 'gpt-5-codex-mini'; + + execClaude('claude', ['--help'], { + CCS_PROFILE_TYPE: 'settings', + CCS_STRIP_INHERITED_ANTHROPIC_ENV: '1', + CLAUDE_CONFIG_DIR: path.join(os.tmpdir(), 'ccs-settings-profile-instance'), + CCS_WEBSEARCH_SKIP: '1', + }); + + expect(spawnCalls.length).toBeGreaterThan(0); + const env = spawnCalls[0].options?.env as NodeJS.ProcessEnv; + expect(env.CCS_PROFILE_TYPE).toBe('settings'); + expect(env.CLAUDE_CONFIG_DIR).toContain('ccs-settings-profile-instance'); + expect(env.ANTHROPIC_BASE_URL).toBeUndefined(); + expect(env.ANTHROPIC_AUTH_TOKEN).toBeUndefined(); + expect(env.ANTHROPIC_API_KEY).toBeUndefined(); + expect(env.ANTHROPIC_MODEL).toBe('gpt-5.4'); + expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-5.4'); + expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.4'); + expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5.4-mini'); + expect(env.ANTHROPIC_SMALL_FAST_MODEL).toBe('gpt-5-codex-mini'); + }); + it('headless executor spawn path strips CLAUDECODE before spawn', async () => { writeConfigWithAutoUpdatePreference(false); process.env.CLAUDECODE = 'nested';