Run Claude Code ↗︎ in a Linux sandbox to edit a GitHub repository from a prompt, then read its changes as a diff. Claude Code calls Anthropic 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. The tutorial builds the runner with Claude Code. Use this page to switch back to Claude Code after you run another agent, or to change its settings. - Anthropic credentials in AI Gateway, either Unified Billing credits or an Anthropic API key stored as a provider key.
-
Replace the
Dockerfile: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 @anthropic-ai/claude-code@2.1.280 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 install script from the Claude Code package puts its native binary in place. The image pins the version, because the flags and event format of Claude Code can change between releases.
-
In
wrangler.jsonc, setMODELto an Anthropic model ID that your gateway can serve:{ "vars": { "AI_GATEWAY_ACCOUNT_ID": "<ACCOUNT_ID>", "AI_GATEWAY_ID": "default", "MODEL": "claude-sonnet-5", }, }[vars] AI_GATEWAY_ACCOUNT_ID = "<ACCOUNT_ID>" AI_GATEWAY_ID = "default" MODEL = "claude-sonnet-5" -
In
src/outbound.ts, make sure theOutboundentrypoint removes thex-api-keyheader before it adds the gateway token:src/outbound.tsts const headers = new Headers(request.headers); headers.delete("x-api-key"); headers.set("cf-aig-authorization", `Bearer ${this.env.AI_GATEWAY_TOKEN}`);Claude Code sends a placeholder API key. If the
x-api-keyheader stays, AI Gateway forwards it to Anthropic, and the model request fails withInvalid API key. -
In
src/sandbox.ts, replace theResultEventconstant with a schema for the final event from Claude Code:src/sandbox.tsts const ResultEvent = z.object({ type: z.literal("result"), subtype: z.string(), is_error: z.boolean(), result: z.string().optional(), }); -
Replace the
agentCommand()andagentEnv()methods:src/sandbox.tsts private agentCommand(prompt: string): string[] { return [ "claude", "--print", "--output-format", "stream-json", "--verbose", "--dangerously-skip-permissions", "--no-session-persistence", "--model", this.env.MODEL, "--", prompt, ]; } private agentEnv(): Record<string, string> { return { ANTHROPIC_BASE_URL: `https://gateway.ai.cloudflare.com/v1/${this.env.AI_GATEWAY_ACCOUNT_ID}/${this.env.AI_GATEWAY_ID}/anthropic`, ANTHROPIC_API_KEY: "provided-by-worker", CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1", IS_SANDBOX: "1", }; }--printruns Claude Code without an interactive terminal.--output-format stream-json --verbosewrites one JSON event per line, including a finalresultevent.--dangerously-skip-permissionslets Claude Code run commands and edit files without asking. The container limits what its commands can reach. Claude Code requiresIS_SANDBOX=1to allow this asroot.ANTHROPIC_BASE_URLsends model requests to the Anthropic endpoint of your gateway.ANTHROPIC_API_KEYis a placeholder, because Claude Code does not start without an API key.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFICturns off update checks, telemetry, and error reporting, so Claude Code calls only your gateway.
If the repository has a
CLAUDE.mdfile, Claude Code reads it, so the instructions in that file apply to the task. -
Replace the
outcome()method:src/sandbox.tsts export class AgentSandbox extends DurableObject<Env> { // ... private async outcome(exitCode: number): Promise<TaskStatus> { let result: z.infer<typeof ResultEvent> | undefined; const events = await this.files.readFile(eventsPath); for await (const line of readLines(events.body)) { const parsed = ResultEvent.safeParse(parseJson(line)); if (parsed.success) { result = parsed.data; } } if (result === undefined) { const stderr = (await this.readOptionalText(stderrPath)) ?? ""; return { state: "failed", error: `claude exited with ${exitCode}: ${stderr.slice(-2000)}`, }; } if (result.is_error) { return { state: "failed", error: result.result ?? result.subtype }; } return { state: "succeeded", result: result.result ?? "" }; } }Claude Code exits successfully even when a model request fails, so the outcome comes from the
is_errorfield of the finalresultevent instead of the exit code. Claude Code retries a failing model request 10 times. A rejected gateway token fails the task after about three minutes withFailed to authenticate. API Error: 401 Unauthorized. -
Deploy your Worker:
npx wrangler deployyarn wrangler deploypnpm wrangler deployA running container keeps the image it started with, so use a new sandbox name for Claude Code.
-
Clone a repository into a sandbox named
claude-1:curl "$WORKER_URL/sandboxes/claude-1/repository" \ --json '{"url": "https://github.com/octocat/Hello-World"}' -
Start a task:
curl "$WORKER_URL/sandboxes/claude-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/claude-1/task"A
succeededtask includes the final reply from Claude Code. -
Read the changes:
curl "$WORKER_URL/sandboxes/claude-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.
- Claude Code example ↗︎: a separate deployable Worker that runs Claude Code.
- Claude Code in AI Gateway: route Claude Code through AI Gateway from other environments.