diff --git a/src/ccs.ts b/src/ccs.ts index 6706c057..54076490 100644 --- a/src/ccs.ts +++ b/src/ccs.ts @@ -37,7 +37,7 @@ import { handleShellCompletionCommand } from './commands/shell-completion-comman import { handleUpdateCommand } from './commands/update-command'; // Import extracted utility functions -import { execClaude, escapeShellArg } from './utils/shell-executor'; +import { execClaude, escapeShellArg, stripClaudeCodeEnv } from './utils/shell-executor'; import { wireChildProcessSignals } from './utils/signal-forwarder'; // Import target adapter system @@ -197,13 +197,13 @@ async function execClaudeWithProxy( const needsShell = isWindows && /\.(cmd|bat)$/i.test(claudeCli); const webSearchEnv = getWebSearchHookEnv(); const imageAnalysisEnv = getImageAnalysisHookEnv(profileName); - const env = { + const env = stripClaudeCodeEnv({ ...process.env, ...envVars, ...webSearchEnv, ...imageAnalysisEnv, CCS_PROFILE_TYPE: 'settings', // Signal to WebSearch hook this is a third-party provider - }; + }); let claude: ChildProcess; if (isPowerShellScript) { diff --git a/src/delegation/headless-executor.ts b/src/delegation/headless-executor.ts index 9e2ad2c3..a4695560 100644 --- a/src/delegation/headless-executor.ts +++ b/src/delegation/headless-executor.ts @@ -17,6 +17,7 @@ import { StreamBuffer, formatToolVerbose } from './executor/stream-parser'; import { buildExecutionResult } from './executor/result-aggregator'; import { getCcsDir, getModelDisplayName } from '../utils/config-manager'; import { getProfileLookupCandidates } from '../utils/profile-compat'; +import { stripClaudeCodeEnv } from '../utils/shell-executor'; // Re-export types for consumers export type { ExecutionOptions, ExecutionResult, StreamMessage } from './executor/types'; @@ -209,7 +210,7 @@ export class HeadlessExecutor { // Strip Claude Code nested session guard env var to allow CCS delegation // (Claude Code v2.1.39+ sets CLAUDECODE to detect nested sessions) - const { CLAUDECODE: _nested, ...cleanEnv } = process.env; + const cleanEnv = stripClaudeCodeEnv(process.env); const proc = spawn(claudeCli, args, { cwd, diff --git a/src/utils/shell-executor.ts b/src/utils/shell-executor.ts index e8a8b93f..d8ea89d3 100644 --- a/src/utils/shell-executor.ts +++ b/src/utils/shell-executor.ts @@ -24,6 +24,22 @@ export function stripAnthropicEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv { return result; } +/** + * Strip Claude Code nested-session guard env var from a process environment. + * + * Note: Windows env keys are case-insensitive, so remove case-insensitively + * to avoid missing variants like `claudecode`. + */ +export function stripClaudeCodeEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv { + const result: NodeJS.ProcessEnv = {}; + for (const key of Object.keys(env)) { + if (key.toUpperCase() !== 'CLAUDECODE') { + result[key] = env[key]; + } + } + return result; +} + /** * Escape arguments for shell execution (cross-platform) * @@ -80,13 +96,13 @@ export function execClaude( : process.env; // Prepare environment (merge with base env if envVars provided) - const env = envVars + const mergedEnv = envVars ? { ...baseEnv, ...envVars, ...webSearchEnv } : { ...baseEnv, ...webSearchEnv }; // Strip Claude Code nested session guard env var to allow CCS delegation // (Claude Code v2.1.39+ sets CLAUDECODE to detect nested sessions) - delete env.CLAUDECODE; + const env = stripClaudeCodeEnv(mergedEnv); // propagate key env vars to tmux session so agent team teammates // (spawned via tmux split-window) inherit the correct config dir