Run the Codex CLI ↗︎ in a Linux sandbox to edit a GitHub repository from a prompt, then read its changes as a diff. Codex calls OpenAI models through AI Gateway. Your Worker adds the gateway token to those requests, and the sandbox can reach only your gateway and github.com.
- The
sandbox-coding-agentproject from Build a coding agent runner. This page replaces the image, the model, and the parts of the Durable Object that are specific to the agent. - OpenAI credentials in AI Gateway, either Unified Billing credits or an OpenAI API key stored as a provider key.
-
Replace the
Dockerfile. Only the highlighted line differs from theDockerfilein the tutorial:Dockerfiledockerfile FROM node:24-trixie-slim RUN apt-get update \ && apt-get install -y --no-install-recommends bash ca-certificates git ripgrep \ && rm -rf /var/lib/apt/lists/* RUN npm install --global --ignore-scripts @openai/codex@0.156.1 COPY --from=docker.io/cloudflare/sandbox:1.0.0 /usr/local/bin/sandbox-shim /usr/local/bin/sandbox-shim WORKDIR /workspace CMD ["sleep", "infinity"]The Codex npm package ships its native binaries as dependencies, so it installs without running install scripts. The image pins the version, because the flags and event format of Codex can change between releases.
-
In
wrangler.jsonc, setMODELto an OpenAI model ID that your gateway can serve:{ "vars": { "AI_GATEWAY_ACCOUNT_ID": "<ACCOUNT_ID>", "AI_GATEWAY_ID": "default", "MODEL": "gpt-6-sol", }, }[vars] AI_GATEWAY_ACCOUNT_ID = "<ACCOUNT_ID>" AI_GATEWAY_ID = "default" MODEL = "gpt-6-sol"Codex sends requests in the OpenAI Responses format, so it works only with OpenAI models that support the Responses API.
-
In
src/sandbox.ts, replace theResultEventconstant with a path for the final message from Codex and a schema for its failure events:src/sandbox.tsts const lastMessagePath = `${taskDirectory}/last-message.txt`; const FailureEvent = z.union([ z.object({ type: z.literal("turn.failed"), error: z.object({ message: z.string() }), }), z.object({ type: z.literal("error"), message: z.string() }), ]); -
Replace the
agentCommand()andagentEnv()methods:src/sandbox.tsts export class AgentSandbox extends DurableObject<Env> { // ... private agentCommand(prompt: string): string[] { const baseUrl = `https://gateway.ai.cloudflare.com/v1/${this.env.AI_GATEWAY_ACCOUNT_ID}/${this.env.AI_GATEWAY_ID}/openai`; return [ "codex", // Run one task without an interactive terminal "exec", // Write one JSON event per line to the events file "--json", // Skip the session files that Codex would otherwise save "--ephemeral", "--dangerously-bypass-approvals-and-sandbox", // Write the final message to a file outside the repository "--output-last-message", lastMessagePath, "--model", this.env.MODEL, // Add a custom provider that sends requests to the OpenAI endpoint of your gateway "--config", 'model_provider="cloudflare-ai-gateway"', "--config", `model_providers.cloudflare-ai-gateway={ name = "Cloudflare AI Gateway", base_url = ${JSON.stringify(baseUrl)}, wire_api = "responses" }`, // Stop the analytics and update requests from Codex "--config", "analytics.enabled=false", "--config", "check_for_update_on_startup=false", // Stop Codex from syncing its plugin marketplace, which clones a // repository from `github.com` on every run "--config", "features.plugins=false", "--", prompt, ]; } private agentEnv(): Record<string, string> { return {}; } }--dangerously-bypass-approvals-and-sandboxturns off the approval prompts and the built-in command sandbox of Codex. The container limits what commands from Codex can reach instead.A custom provider sends no API key, and the
Outboundentrypoint adds the gateway token.Codex reads the certificate for intercepted HTTPS from
SSL_CERT_FILE, whichtrustEnvin the runner sets. -
Replace the
outcome()method:src/sandbox.tsts export class AgentSandbox extends DurableObject<Env> { // ... private async outcome(exitCode: number): Promise<TaskStatus> { if (exitCode === 0) { return { state: "succeeded", result: (await this.readOptionalText(lastMessagePath)) ?? "", }; } let failure: string | undefined; const events = await this.files.readFile(eventsPath); for await (const line of readLines(events.body)) { const parsed = FailureEvent.safeParse(parseJson(line)); if (parsed.success) { failure = parsed.data.type === "error" ? parsed.data.message : parsed.data.error.message; } } if (failure === undefined) { const stderr = (await this.readOptionalText(stderrPath)) ?? ""; return { state: "failed", error: `codex exited with ${exitCode}: ${stderr.slice(-2000)}`, }; } return { state: "failed", error: failure }; } }Codex exits with a nonzero code when its turn fails, so the exit code decides the outcome. A failed task returns the message from the last
turn.failedorerrorevent. Codex retries a failing model request five times, and the task fails after about 20 seconds. -
Deploy your Worker:
npx wrangler deployyarn wrangler deploypnpm wrangler deployThe
AI_GATEWAY_TOKENsecret from the tutorial stays in place. A running container keeps the image it started with, so use a new sandbox name for Codex.
-
Clone a repository into a sandbox named
codex-1:curl "$WORKER_URL/sandboxes/codex-1/repository" \ --json '{"url": "https://github.com/octocat/Hello-World"}' -
Start a task:
curl "$WORKER_URL/sandboxes/codex-1/task" \ --data "Add a file NOTES.md with a one-sentence summary of this repository. Do not commit." -
Check the task until its state is
succeededorfailed:curl "$WORKER_URL/sandboxes/codex-1/task"A
succeededtask includes the final message that Codex wrote. -
Read the changes:
curl "$WORKER_URL/sandboxes/codex-1/diff"The diff shows the new
NOTES.mdfile.
- Build a coding agent runner: how the runner starts the sandbox, runs the task, and reads the diff.
- Codex example ↗︎: a separate deployable Worker that runs Codex.
- OpenAI Codex in AI Gateway: route Codex through AI Gateway from your own terminal.