diff --git a/src/__tests__/integration/group-chat-integration.test.ts b/src/__tests__/integration/group-chat-integration.test.ts index 2362744936..42ac1aabf5 100644 --- a/src/__tests__/integration/group-chat-integration.test.ts +++ b/src/__tests__/integration/group-chat-integration.test.ts @@ -123,13 +123,15 @@ const AGENTS: AgentConfig[] = [ * Codex does NOT support --input-format stream-json (supportsStreamJsonInput: false) */ buildArgs: (prompt: string, options?: { images?: string[] }) => { + // `-C` precedes `exec` because Codex treats it as a root-level global + // flag — placing it after the subcommand makes it silently ignored (#959). const args = [ + '-C', + TEST_CWD, 'exec', '--dangerously-bypass-approvals-and-sandbox', '--skip-git-repo-check', '--json', - '-C', - TEST_CWD, ]; // IMPORTANT: This mirrors process-manager.ts logic diff --git a/src/__tests__/integration/provider-integration.test.ts b/src/__tests__/integration/provider-integration.test.ts index 290b18d293..420e5addcf 100644 --- a/src/__tests__/integration/provider-integration.test.ts +++ b/src/__tests__/integration/provider-integration.test.ts @@ -372,20 +372,21 @@ const PROVIDERS: ProviderConfig[] = [ */ buildInitialArgs: (prompt: string, options?: { images?: string[] }) => { // Codex arg order from process.ts IPC handler: - // 1. batchModePrefix: ['exec'] - // 2. base args: [] (empty for Codex) - // 3. batchModeArgs: ['--dangerously-bypass-approvals-and-sandbox', '--skip-git-repo-check'] - // 4. jsonOutputArgs: ['--json'] - // 5. workingDirArgs: ['-C', dir] + // 1. workingDirArgs: ['-C', dir] (must precede the `exec` subcommand + // because Codex treats `-C` as a root-level global flag — see #959) + // 2. batchModePrefix: ['exec'] + // 3. base args: [] (empty for Codex) + // 4. batchModeArgs: ['--dangerously-bypass-approvals-and-sandbox', '--skip-git-repo-check'] + // 5. jsonOutputArgs: ['--json'] // 6. prompt via '--' separator (process-manager.ts) const args = [ + '-C', + TEST_CWD, 'exec', '--dangerously-bypass-approvals-and-sandbox', '--skip-git-repo-check', '--json', - '-C', - TEST_CWD, ]; // IMPORTANT: This mirrors process-manager.ts logic @@ -404,12 +405,12 @@ const PROVIDERS: ProviderConfig[] = [ return [...args, '--', prompt]; }, buildResumeArgs: (sessionId: string, prompt: string) => [ + '-C', + TEST_CWD, 'exec', '--dangerously-bypass-approvals-and-sandbox', '--skip-git-repo-check', '--json', - '-C', - TEST_CWD, 'resume', sessionId, '--', @@ -420,20 +421,20 @@ const PROVIDERS: ProviderConfig[] = [ * Codex uses file-based image args (-i) - images decoded on remote via script. */ buildSshArgs: () => [ + '-C', + TEST_CWD, 'exec', '--dangerously-bypass-approvals-and-sandbox', '--skip-git-repo-check', '--json', - '-C', - TEST_CWD, ], buildSshResumeArgs: (sessionId: string) => [ + '-C', + TEST_CWD, 'exec', '--dangerously-bypass-approvals-and-sandbox', '--skip-git-repo-check', '--json', - '-C', - TEST_CWD, 'resume', sessionId, ], @@ -487,12 +488,12 @@ const PROVIDERS: ProviderConfig[] = [ * Mirrors agent-detector.ts: imageArgs: (imagePath) => ['-i', imagePath] */ buildImageArgs: (prompt: string, imagePath: string) => [ + '-C', + TEST_CWD, 'exec', '--dangerously-bypass-approvals-and-sandbox', '--skip-git-repo-check', '--json', - '-C', - TEST_CWD, '-i', imagePath, '--', diff --git a/src/__tests__/main/utils/agent-args.test.ts b/src/__tests__/main/utils/agent-args.test.ts index a1593e57fe..06a9c3b525 100644 --- a/src/__tests__/main/utils/agent-args.test.ts +++ b/src/__tests__/main/utils/agent-args.test.ts @@ -102,7 +102,9 @@ describe('buildAgentArgs', () => { }); // -- workingDirArgs -- - it('adds workingDirArgs when cwd provided', () => { + it('prepends workingDirArgs when cwd provided', () => { + // Codex treats `-C` as a root-level global flag — it must appear before + // any subcommand (e.g. `exec`) or it is silently ignored (#959). const agent = makeAgent({ workingDirArgs: (dir: string) => ['-C', dir], }); @@ -110,7 +112,21 @@ describe('buildAgentArgs', () => { baseArgs: ['--print'], cwd: '/home/user/project', }); - expect(result).toEqual(['--print', '-C', '/home/user/project']); + expect(result).toEqual(['-C', '/home/user/project', '--print']); + }); + + it('places workingDirArgs before batchModePrefix subcommand', () => { + // Regression: -C must land before `exec` so Codex picks up the cwd. + const agent = makeAgent({ + batchModePrefix: ['exec'], + workingDirArgs: (dir: string) => ['-C', dir], + }); + const result = buildAgentArgs(agent, { + baseArgs: ['--json'], + prompt: 'do stuff', + cwd: '/home/user/project', + }); + expect(result).toEqual(['-C', '/home/user/project', 'exec', '--json']); }); it('does not add workingDirArgs when cwd is not provided', () => { @@ -253,14 +269,16 @@ describe('buildAgentArgs', () => { }); // batchModeArgs (--skip-git) is omitted when readOnlyMode is true — - // batch mode args grant write/approval permissions that conflict with read-only + // batch mode args grant write/approval permissions that conflict with read-only. + // workingDirArgs (-C /tmp) is prepended so the directory flag lands before + // the batchModePrefix subcommand (#959). expect(result).toEqual([ + '-C', + '/tmp', 'run', '--print', '--format', 'json', - '-C', - '/tmp', '--agent', 'plan', '--model', diff --git a/src/cli/services/agent-spawner.ts b/src/cli/services/agent-spawner.ts index 6e7a4a0f21..092eef1efa 100644 --- a/src/cli/services/agent-spawner.ts +++ b/src/cli/services/agent-spawner.ts @@ -357,6 +357,12 @@ async function spawnJsonLineAgent( // Build args from agent definition const args: string[] = []; + // Codex requires explicit working directory arg (other agents use process cwd). + // Must come before batchModePrefix because Codex treats `-C` as a root-level + // global flag that's silently ignored if it appears after the `exec` subcommand (#959). + if (toolType === 'codex' && def?.workingDirArgs) { + args.push(...def.workingDirArgs(cwd)); + } if (def?.batchModePrefix) args.push(...def.batchModePrefix); if (def?.batchModeArgs) args.push(...def.batchModeArgs); if (def?.jsonOutputArgs) args.push(...def.jsonOutputArgs); @@ -365,11 +371,6 @@ async function spawnJsonLineAgent( args.push(...def.resumeArgs(agentSessionId)); } - // Codex requires explicit working directory arg (other agents use process cwd) - if (toolType === 'codex' && def?.workingDirArgs) { - args.push(...def.workingDirArgs(cwd)); - } - // Add prompt (with or without '--' separator depending on agent) if (!def?.noPromptSeparator) { args.push('--'); diff --git a/src/main/agents/definitions.ts b/src/main/agents/definitions.ts index 8693830459..5d81793e89 100644 --- a/src/main/agents/definitions.ts +++ b/src/main/agents/definitions.ts @@ -148,7 +148,9 @@ export const AGENT_DEFINITIONS: AgentDefinition[] = [ // Base args for interactive mode (no flags that are exec-only) args: [], // Codex CLI argument builders - // Batch mode: codex exec --json --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check [--sandbox read-only] [-C dir] [resume ] -- "prompt" + // Batch mode: codex [-C dir] exec --json --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check [--sandbox read-only] [resume ] -- "prompt" + // `-C` is a root-level global flag and MUST appear before the `exec` subcommand + // or Codex silently ignores it (see #959). buildAgentArgs prepends workingDirArgs accordingly. // Sandbox modes: // - Default (YOLO): --dangerously-bypass-approvals-and-sandbox (full system access, required by Maestro) // - Read-only: --sandbox read-only (can only read files, overrides YOLO) diff --git a/src/main/utils/agent-args.ts b/src/main/utils/agent-args.ts index 222487120e..c33feb4ec4 100644 --- a/src/main/utils/agent-args.ts +++ b/src/main/utils/agent-args.ts @@ -71,7 +71,10 @@ export function buildAgentArgs( } if (agent.workingDirArgs && options.cwd) { - finalArgs = [...finalArgs, ...agent.workingDirArgs(options.cwd)]; + // Prepend so the directory flag lands before any subcommand (e.g. Codex + // `exec`). Codex treats `-C` as a root-level global flag — placing it + // after the subcommand makes it silently ignored (#959). + finalArgs = [...agent.workingDirArgs(options.cwd), ...finalArgs]; } if (options.readOnlyMode && agent.readOnlyArgs) {