diff --git a/codex-cli/src/app.tsx b/codex-cli/src/app.tsx index c0b8c6f4..5d859db5 100644 --- a/codex-cli/src/app.tsx +++ b/codex-cli/src/app.tsx @@ -22,6 +22,7 @@ type Props = { imagePaths?: Array; rollout?: AppRollout; approvalPolicy: ApprovalPolicy; + additionalWritableRoots: ReadonlyArray; fullStdout: boolean; }; @@ -31,6 +32,7 @@ export default function App({ rollout, imagePaths, approvalPolicy, + additionalWritableRoots, fullStdout, }: Props): JSX.Element { const app = useApp(); @@ -97,6 +99,7 @@ export default function App({ prompt={prompt} imagePaths={imagePaths} approvalPolicy={approvalPolicy} + additionalWritableRoots={additionalWritableRoots} fullStdout={fullStdout} /> ); diff --git a/codex-cli/src/cli.tsx b/codex-cli/src/cli.tsx index da6a465a..b0bef3aa 100644 --- a/codex-cli/src/cli.tsx +++ b/codex-cli/src/cli.tsx @@ -53,13 +53,14 @@ const cli = meow( $ codex completion Options - -h, --help Show usage and exit - -m, --model Model to use for completions (default: o4-mini) - -i, --image Path(s) to image files to include as input - -v, --view Inspect a previously saved rollout instead of starting a session - -q, --quiet Non-interactive mode that only prints the assistant's final output - -c, --config Open the instructions file in your editor - -a, --approval-mode Override the approval policy: 'suggest', 'auto-edit', or 'full-auto' + -h, --help Show usage and exit + -m, --model Model to use for completions (default: o4-mini) + -i, --image Path(s) to image files to include as input + -v, --view Inspect a previously saved rollout instead of starting a session + -q, --quiet Non-interactive mode that only prints the assistant's final output + -c, --config Open the instructions file in your editor + -w, --writable-root Writable folder for sandbox in full-auto mode (can be specified multiple times) + -a, --approval-mode Override the approval policy: 'suggest', 'auto-edit', or 'full-auto' --auto-edit Automatically approve file edits; still prompt for commands --full-auto Automatically approve edits and commands when executed in the sandbox @@ -122,6 +123,13 @@ const cli = meow( description: "Determine the approval mode for Codex (default: suggest) Values: suggest, auto-edit, full-auto", }, + writableRoot: { + type: "string", + isMultiple: true, + aliases: ["w"], + description: + "Writable folder for sandbox in full-auto mode (can be specified multiple times)", + }, noProjectDoc: { type: "boolean", description: "Disable automatic inclusion of project‑level codex.md", @@ -276,6 +284,11 @@ if (fullContextMode) { process.exit(0); } +// Ensure that all values in additionalWritableRoots are absolute paths. +const additionalWritableRoots: ReadonlyArray = ( + cli.flags.writableRoot ?? [] +).map((p) => path.resolve(p)); + // If we are running in --quiet mode, do that and exit. const quietMode = Boolean(cli.flags.quiet); const autoApproveEverything = Boolean( @@ -298,6 +311,7 @@ if (quietMode) { approvalPolicy: autoApproveEverything ? AutoApprovalMode.FULL_AUTO : AutoApprovalMode.SUGGEST, + additionalWritableRoots, config, }); onExit(); @@ -332,6 +346,7 @@ const instance = render( rollout={rollout} imagePaths={imagePaths} approvalPolicy={approvalPolicy} + additionalWritableRoots={additionalWritableRoots} fullStdout={fullStdout} />, { @@ -393,11 +408,13 @@ async function runQuietMode({ prompt, imagePaths, approvalPolicy, + additionalWritableRoots, config, }: { prompt: string; imagePaths: Array; approvalPolicy: ApprovalPolicy; + additionalWritableRoots: ReadonlyArray; config: AppConfig; }): Promise { const agent = new AgentLoop({ @@ -405,6 +422,7 @@ async function runQuietMode({ config: config, instructions: config.instructions, approvalPolicy, + additionalWritableRoots, onItem: (item: ResponseItem) => { // eslint-disable-next-line no-console console.log(formatResponseItemForQuietMode(item)); diff --git a/codex-cli/src/components/chat/terminal-chat.tsx b/codex-cli/src/components/chat/terminal-chat.tsx index b572fddf..e209c61a 100644 --- a/codex-cli/src/components/chat/terminal-chat.tsx +++ b/codex-cli/src/components/chat/terminal-chat.tsx @@ -37,6 +37,7 @@ type Props = { prompt?: string; imagePaths?: Array; approvalPolicy: ApprovalPolicy; + additionalWritableRoots: ReadonlyArray; fullStdout: boolean; }; @@ -122,6 +123,7 @@ export default function TerminalChat({ prompt: _initialPrompt, imagePaths: _initialImagePaths, approvalPolicy: initialApprovalPolicy, + additionalWritableRoots, fullStdout, }: Props): React.ReactElement { const [model, setModel] = useState(config.model); @@ -183,6 +185,7 @@ export default function TerminalChat({ config, instructions: config.instructions, approvalPolicy, + additionalWritableRoots, onLastResponseId: setLastResponseId, onItem: (item) => { log(`onItem: ${JSON.stringify(item)}`); @@ -248,7 +251,13 @@ export default function TerminalChat({ agentRef.current = undefined; forceUpdate(); // re‑render after teardown too }; - }, [model, config, approvalPolicy, requestConfirmation]); + }, [ + model, + config, + approvalPolicy, + requestConfirmation, + additionalWritableRoots, + ]); // whenever loading starts/stops, reset or start a timer — but pause the // timer while a confirmation overlay is displayed so we don't trigger a diff --git a/codex-cli/src/utils/agent/agent-loop.ts b/codex-cli/src/utils/agent/agent-loop.ts index 6c8f1cbf..b9105f64 100644 --- a/codex-cli/src/utils/agent/agent-loop.ts +++ b/codex-cli/src/utils/agent/agent-loop.ts @@ -45,6 +45,9 @@ type AgentLoopParams = { onItem: (item: ResponseItem) => void; onLoading: (loading: boolean) => void; + /** Extra writable roots to use with sandbox execution. */ + additionalWritableRoots: ReadonlyArray; + /** Called when the command is not auto-approved to request explicit user review. */ getCommandConfirmation: ( command: Array, @@ -58,6 +61,7 @@ export class AgentLoop { private instructions?: string; private approvalPolicy: ApprovalPolicy; private config: AppConfig; + private additionalWritableRoots: ReadonlyArray; // Using `InstanceType` sidesteps typing issues with the OpenAI package under // the TS 5+ `moduleResolution=bundler` setup. OpenAI client instance. We keep the concrete @@ -213,6 +217,7 @@ export class AgentLoop { onLoading, getCommandConfirmation, onLastResponseId, + additionalWritableRoots, }: AgentLoopParams & { config?: AppConfig }) { this.model = model; this.instructions = instructions; @@ -229,6 +234,7 @@ export class AgentLoop { model, instructions: instructions ?? "", } as AppConfig); + this.additionalWritableRoots = additionalWritableRoots; this.onItem = onItem; this.onLoading = onLoading; this.getCommandConfirmation = getCommandConfirmation; @@ -358,6 +364,7 @@ export class AgentLoop { args, this.config, this.approvalPolicy, + this.additionalWritableRoots, this.getCommandConfirmation, this.execAbortController?.signal, ); diff --git a/codex-cli/src/utils/agent/exec.ts b/codex-cli/src/utils/agent/exec.ts index a441f192..22b75c02 100644 --- a/codex-cli/src/utils/agent/exec.ts +++ b/codex-cli/src/utils/agent/exec.ts @@ -16,7 +16,12 @@ const DEFAULT_TIMEOUT_MS = 10_000; // 10 seconds * mapped to a non-zero exit code and the error message should be in stderr. */ export function exec( - { cmd, workdir, timeoutInMillis }: ExecInput, + { + cmd, + workdir, + timeoutInMillis, + additionalWritableRoots, + }: ExecInput & { additionalWritableRoots: ReadonlyArray }, sandbox: SandboxType, abortSignal?: AbortSignal, ): Promise { @@ -30,7 +35,12 @@ export function exec( timeout: timeoutInMillis || DEFAULT_TIMEOUT_MS, ...(workdir ? { cwd: workdir } : {}), }; - const writableRoots = [process.cwd(), os.tmpdir()]; + // Merge default writable roots with any user-specified ones. + const writableRoots = [ + process.cwd(), + os.tmpdir(), + ...additionalWritableRoots, + ]; return execForSandbox(cmd, opts, writableRoots, abortSignal); } diff --git a/codex-cli/src/utils/agent/handle-exec-command.ts b/codex-cli/src/utils/agent/handle-exec-command.ts index 28bd9b89..1af390e2 100644 --- a/codex-cli/src/utils/agent/handle-exec-command.ts +++ b/codex-cli/src/utils/agent/handle-exec-command.ts @@ -74,6 +74,7 @@ export async function handleExecCommand( args: ExecInput, config: AppConfig, policy: ApprovalPolicy, + additionalWritableRoots: ReadonlyArray, getCommandConfirmation: ( command: Array, applyPatch: ApplyPatchCommand | undefined, @@ -91,6 +92,7 @@ export async function handleExecCommand( args, /* applyPatch */ undefined, /* runInSandbox */ false, + additionalWritableRoots, abortSignal, ).then(convertSummaryToResult); } @@ -138,6 +140,7 @@ export async function handleExecCommand( args, applyPatch, runInSandbox, + additionalWritableRoots, abortSignal, ); // If the operation was aborted in the meantime, propagate the cancellation @@ -170,7 +173,13 @@ export async function handleExecCommand( } else { // The user has approved the command, so we will run it outside of the // sandbox. - const summary = await execCommand(args, applyPatch, false, abortSignal); + const summary = await execCommand( + args, + applyPatch, + false, + additionalWritableRoots, + abortSignal, + ); return convertSummaryToResult(summary); } } else { @@ -202,6 +211,7 @@ async function execCommand( execInput: ExecInput, applyPatchCommand: ApplyPatchCommand | undefined, runInSandbox: boolean, + additionalWritableRoots: ReadonlyArray, abortSignal?: AbortSignal, ): Promise { let { workdir } = execInput; @@ -239,7 +249,11 @@ async function execCommand( const execResult = applyPatchCommand != null ? execApplyPatch(applyPatchCommand.patch) - : await exec(execInput, await getSandbox(runInSandbox), abortSignal); + : await exec( + { ...execInput, additionalWritableRoots }, + await getSandbox(runInSandbox), + abortSignal, + ); const duration = Date.now() - start; const { stdout, stderr, exitCode } = execResult; diff --git a/codex-cli/tests/agent-cancel-early.test.ts b/codex-cli/tests/agent-cancel-early.test.ts index b235a6d6..1460bb0b 100644 --- a/codex-cli/tests/agent-cancel-early.test.ts +++ b/codex-cli/tests/agent-cancel-early.test.ts @@ -88,6 +88,7 @@ describe("cancel before first function_call", () => { const { _test } = (await import("openai")) as any; const agent = new AgentLoop({ + additionalWritableRoots: [], model: "any", instructions: "", approvalPolicy: { mode: "auto" } as any, diff --git a/codex-cli/tests/agent-cancel-prev-response.test.ts b/codex-cli/tests/agent-cancel-prev-response.test.ts index fe73c338..fbeff0a7 100644 --- a/codex-cli/tests/agent-cancel-prev-response.test.ts +++ b/codex-cli/tests/agent-cancel-prev-response.test.ts @@ -99,6 +99,7 @@ describe("cancel clears previous_response_id", () => { model: "any", instructions: "", approvalPolicy: { mode: "auto" } as any, + additionalWritableRoots: [], onItem: () => {}, onLoading: () => {}, getCommandConfirmation: async () => ({ review: "yes" } as any), diff --git a/codex-cli/tests/agent-cancel-race.test.ts b/codex-cli/tests/agent-cancel-race.test.ts index 89e7cca7..5ae572d1 100644 --- a/codex-cli/tests/agent-cancel-race.test.ts +++ b/codex-cli/tests/agent-cancel-race.test.ts @@ -92,6 +92,7 @@ describe("Agent cancellation race", () => { const items: Array = []; const agent = new AgentLoop({ + additionalWritableRoots: [], model: "any", instructions: "", config: { model: "any", instructions: "" }, diff --git a/codex-cli/tests/agent-cancel.test.ts b/codex-cli/tests/agent-cancel.test.ts index 69c17f7f..2cd01cd6 100644 --- a/codex-cli/tests/agent-cancel.test.ts +++ b/codex-cli/tests/agent-cancel.test.ts @@ -91,6 +91,7 @@ describe("Agent cancellation", () => { instructions: "", config: { model: "any", instructions: "" }, approvalPolicy: { mode: "auto" } as any, + additionalWritableRoots: [], onItem: (item) => { received.push(item); }, @@ -136,6 +137,7 @@ describe("Agent cancellation", () => { const received: Array = []; const agent = new AgentLoop({ + additionalWritableRoots: [], model: "any", instructions: "", config: { model: "any", instructions: "" }, diff --git a/codex-cli/tests/agent-function-call-id.test.ts b/codex-cli/tests/agent-function-call-id.test.ts index d50c08ee..8f35b9bc 100644 --- a/codex-cli/tests/agent-function-call-id.test.ts +++ b/codex-cli/tests/agent-function-call-id.test.ts @@ -118,6 +118,7 @@ describe("function_call_output includes original call ID", () => { model: "any", instructions: "", approvalPolicy: { mode: "auto" } as any, + additionalWritableRoots: [], onItem: () => {}, onLoading: () => {}, getCommandConfirmation: async () => ({ review: "yes" } as any), diff --git a/codex-cli/tests/agent-generic-network-error.test.ts b/codex-cli/tests/agent-generic-network-error.test.ts index 942adff6..1ae06467 100644 --- a/codex-cli/tests/agent-generic-network-error.test.ts +++ b/codex-cli/tests/agent-generic-network-error.test.ts @@ -56,6 +56,7 @@ describe("AgentLoop – generic network/server errors", () => { const received: Array = []; const agent = new AgentLoop({ + additionalWritableRoots: [], model: "any", instructions: "", approvalPolicy: { mode: "auto" } as any, @@ -99,6 +100,7 @@ describe("AgentLoop – generic network/server errors", () => { const received: Array = []; const agent = new AgentLoop({ + additionalWritableRoots: [], model: "any", instructions: "", approvalPolicy: { mode: "auto" } as any, diff --git a/codex-cli/tests/agent-interrupt-continue.test.ts b/codex-cli/tests/agent-interrupt-continue.test.ts index db20bc9c..db4006bc 100644 --- a/codex-cli/tests/agent-interrupt-continue.test.ts +++ b/codex-cli/tests/agent-interrupt-continue.test.ts @@ -34,6 +34,7 @@ describe("Agent interrupt and continue", () => { // Create the agent const agent = new AgentLoop({ + additionalWritableRoots: [], model: "test-model", instructions: "", approvalPolicy: { mode: "auto" } as any, diff --git a/codex-cli/tests/agent-invalid-request-error.test.ts b/codex-cli/tests/agent-invalid-request-error.test.ts index 090d0b52..d6d5f88f 100644 --- a/codex-cli/tests/agent-invalid-request-error.test.ts +++ b/codex-cli/tests/agent-invalid-request-error.test.ts @@ -58,6 +58,7 @@ describe("AgentLoop – invalid request / 4xx errors", () => { model: "any", instructions: "", approvalPolicy: { mode: "auto" } as any, + additionalWritableRoots: [], onItem: (i) => received.push(i), onLoading: () => {}, getCommandConfirmation: async () => ({ review: "yes" } as any), diff --git a/codex-cli/tests/agent-max-tokens-error.test.ts b/codex-cli/tests/agent-max-tokens-error.test.ts index de4fd170..82cdc1df 100644 --- a/codex-cli/tests/agent-max-tokens-error.test.ts +++ b/codex-cli/tests/agent-max-tokens-error.test.ts @@ -58,6 +58,7 @@ describe("AgentLoop – max_tokens too large error", () => { const received: Array = []; const agent = new AgentLoop({ + additionalWritableRoots: [], model: "any", instructions: "", approvalPolicy: { mode: "auto" } as any, diff --git a/codex-cli/tests/agent-network-errors.test.ts b/codex-cli/tests/agent-network-errors.test.ts index f98ea5bf..236c18f6 100644 --- a/codex-cli/tests/agent-network-errors.test.ts +++ b/codex-cli/tests/agent-network-errors.test.ts @@ -109,6 +109,7 @@ describe("AgentLoop – network resilience", () => { model: "any", instructions: "", approvalPolicy: { mode: "auto" } as any, + additionalWritableRoots: [], onItem: (i) => received.push(i), onLoading: () => {}, getCommandConfirmation: async () => ({ review: "yes" } as any), @@ -150,6 +151,7 @@ describe("AgentLoop – network resilience", () => { model: "any", instructions: "", approvalPolicy: { mode: "auto" } as any, + additionalWritableRoots: [], onItem: (i) => received.push(i), onLoading: () => {}, getCommandConfirmation: async () => ({ review: "yes" } as any), diff --git a/codex-cli/tests/agent-project-doc.test.ts b/codex-cli/tests/agent-project-doc.test.ts index d3050f39..4b8951e7 100644 --- a/codex-cli/tests/agent-project-doc.test.ts +++ b/codex-cli/tests/agent-project-doc.test.ts @@ -112,6 +112,7 @@ describe("AgentLoop", () => { expect(config.instructions).toContain("Hello docs!"); const agent = new AgentLoop({ + additionalWritableRoots: [], model: "o3", // arbitrary instructions: config.instructions, config, diff --git a/codex-cli/tests/agent-rate-limit-error.test.ts b/codex-cli/tests/agent-rate-limit-error.test.ts index 97827446..4a8cbe1d 100644 --- a/codex-cli/tests/agent-rate-limit-error.test.ts +++ b/codex-cli/tests/agent-rate-limit-error.test.ts @@ -79,6 +79,7 @@ describe("AgentLoop – rate‑limit handling", () => { model: "any", instructions: "", approvalPolicy: { mode: "auto" } as any, + additionalWritableRoots: [], onItem: (i) => received.push(i), onLoading: () => {}, getCommandConfirmation: async () => ({ review: "yes" } as any), diff --git a/codex-cli/tests/agent-server-retry.test.ts b/codex-cli/tests/agent-server-retry.test.ts index 09278f2c..954d5f82 100644 --- a/codex-cli/tests/agent-server-retry.test.ts +++ b/codex-cli/tests/agent-server-retry.test.ts @@ -97,6 +97,7 @@ describe("AgentLoop – automatic retry on 5xx errors", () => { model: "any", instructions: "", approvalPolicy: { mode: "auto" } as any, + additionalWritableRoots: [], onItem: (i) => received.push(i), onLoading: () => {}, getCommandConfirmation: async () => ({ review: "yes" } as any), @@ -134,6 +135,7 @@ describe("AgentLoop – automatic retry on 5xx errors", () => { model: "any", instructions: "", approvalPolicy: { mode: "auto" } as any, + additionalWritableRoots: [], onItem: (i) => received.push(i), onLoading: () => {}, getCommandConfirmation: async () => ({ review: "yes" } as any), diff --git a/codex-cli/tests/agent-terminate.test.ts b/codex-cli/tests/agent-terminate.test.ts index bce77437..634245bd 100644 --- a/codex-cli/tests/agent-terminate.test.ts +++ b/codex-cli/tests/agent-terminate.test.ts @@ -82,7 +82,14 @@ describe("Agent terminate (hard cancel)", () => { it("suppresses function_call_output and stops processing once terminate() is invoked", async () => { // Simulate a long‑running exec that would normally resolve with output. vi.spyOn(handleExec, "handleExecCommand").mockImplementation( - async (_args, _config, _policy, _getConf, abortSignal) => { + async ( + _args, + _config, + _policy, + _additionalWritableRoots, + _getConf, + abortSignal, + ) => { // Wait until the abort signal is fired or 2s (whichever comes first). await new Promise((resolve) => { if (abortSignal?.aborted) { @@ -106,6 +113,7 @@ describe("Agent terminate (hard cancel)", () => { instructions: "", config: { model: "any", instructions: "" }, approvalPolicy: { mode: "auto" } as any, + additionalWritableRoots: [], onItem: (item) => received.push(item), onLoading: () => {}, getCommandConfirmation: async () => ({ review: "yes" } as any), @@ -141,6 +149,7 @@ describe("Agent terminate (hard cancel)", () => { instructions: "", config: { model: "any", instructions: "" }, approvalPolicy: { mode: "auto" } as any, + additionalWritableRoots: [], onItem: () => {}, onLoading: () => {}, getCommandConfirmation: async () => ({ review: "yes" } as any), diff --git a/codex-cli/tests/agent-thinking-time.test.ts b/codex-cli/tests/agent-thinking-time.test.ts index 71320700..9fa7bc86 100644 --- a/codex-cli/tests/agent-thinking-time.test.ts +++ b/codex-cli/tests/agent-thinking-time.test.ts @@ -107,6 +107,7 @@ describe("thinking time counter", () => { model: "any", instructions: "", approvalPolicy: { mode: "auto" } as any, + additionalWritableRoots: [], onItem: (i) => items.push(i), onLoading: () => {}, getCommandConfirmation: async () => ({ review: "yes" } as any), diff --git a/codex-cli/tests/invalid-command-handling.test.ts b/codex-cli/tests/invalid-command-handling.test.ts index a3f87a72..e5b4261e 100644 --- a/codex-cli/tests/invalid-command-handling.test.ts +++ b/codex-cli/tests/invalid-command-handling.test.ts @@ -53,10 +53,12 @@ describe("handleExecCommand – invalid executable", () => { const policy = { mode: "auto" } as any; const getConfirmation = async () => ({ review: "yes" } as any); + const additionalWritableRoots: Array = []; const { outputText, metadata } = await handleExecCommand( execInput, config, policy, + additionalWritableRoots, getConfirmation, );