Run OpenCode ↗︎ in a Linux sandbox to edit a GitHub repository from a prompt, then read its changes as a diff. OpenCode calls 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. - Anthropic credentials in AI Gateway, either Unified Billing credits or an Anthropic API key stored as a provider key.
-
Replace the
Dockerfile. Only the highlighted lines differ 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 opencode-linux-x64-baseline@1.18.32 \ && ln -s /usr/local/lib/node_modules/opencode-linux-x64-baseline/bin/opencode /usr/local/bin/opencode 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 image installs the baseline x64 OpenCode binary directly. The baseline binary runs on every x64 CPU. The
opencode-aipackage picks a binary for the CPU of the machine that builds the image. Anamd64build on another architecture runs under an emulator, where the package can pick the wrong binary.OpenCode searches files with
rgfrom the image. Withoutripgrep, it tries to downloadrgfrom GitHub. The image pins the OpenCode version, because the model list and event format of OpenCode can change between releases. -
In
wrangler.jsonc, setMODELto an OpenCode model ID for AI Gateway:{ "vars": { "AI_GATEWAY_ACCOUNT_ID": "<ACCOUNT_ID>", "AI_GATEWAY_ID": "default", "MODEL": "cloudflare-ai-gateway/anthropic/claude-sonnet-5", }, }[vars] AI_GATEWAY_ACCOUNT_ID = "<ACCOUNT_ID>" AI_GATEWAY_ID = "default" MODEL = "cloudflare-ai-gateway/anthropic/claude-sonnet-5"OpenCode model IDs for AI Gateway have the form
cloudflare-ai-gateway/<PROVIDER>/<MODEL>. OpenCode uses the model list built into the pinned release. To print that list, runopencode models cloudflare-ai-gatewaywith the same OpenCode version. -
In
src/sandbox.ts, replace theResultEventconstant with a schema for the OpenCode events that carry the reply and errors:src/sandbox.tsts const OpencodeEvent = z.union([ z.object({ type: z.literal("text"), part: z.object({ text: z.string() }) }), z.object({ type: z.literal("error"), error: z.object({ name: z.string(), data: z.object({ message: z.string().optional() }).optional(), }), }), ]); -
Replace the
agentCommand()andagentEnv()methods:src/sandbox.tsts export class AgentSandbox extends DurableObject<Env> { // ... private agentCommand(prompt: string): string[] { return [ "opencode", // Run one task without an interactive terminal "run", // Write one JSON event per line to the events file "--format", "json", "--auto", // Select the AI Gateway model from `MODEL` "--model", this.env.MODEL, "--", prompt, ]; } private agentEnv(): Record<string, string> { return { CLOUDFLARE_API_TOKEN: "provided-by-worker", CLOUDFLARE_ACCOUNT_ID: this.env.AI_GATEWAY_ACCOUNT_ID, CLOUDFLARE_GATEWAY_ID: this.env.AI_GATEWAY_ID, OPENCODE_DISABLE_MODELS_FETCH: "1", OPENCODE_DISABLE_AUTOUPDATE: "1", OPENCODE_DISABLE_DEFAULT_PLUGINS: "1", OPENCODE_DISABLE_LSP_DOWNLOAD: "1", OPENCODE_DISABLE_SHARE: "1", }; } }--autoapproves every tool call that the OpenCode configuration does not deny. Without it, OpenCode rejects permission requests in a run without a terminal. The container limits what commands from OpenCode can reach.The built-in
cloudflare-ai-gatewayprovider in OpenCode reads the account and gateway IDs from the environment. For Anthropic models, it sends requests to the universal endpoint of the gateway: the gateway path with no suffix. TheOutboundentrypoint in the runner allows that path and the paths under it.CLOUDFLARE_API_TOKENis a placeholder, because OpenCode does not start without a token. TheOutboundentrypoint replaces the token header with the gateway token.The
OPENCODE_DISABLE_*variables make OpenCode use the model list built into the binary and turn off its update check, default plugins, language server downloads, and sharing. OpenCode still tries to install@opencode-ai/pluginfrom npm in the background. That request gets a403response from theOutboundentrypoint, and OpenCode continues. -
Replace the
outcome()method:src/sandbox.tsts export class AgentSandbox extends DurableObject<Env> { // ... private async outcome(exitCode: number): Promise<TaskStatus> { let text = ""; let error: string | undefined; const events = await this.files.readFile(eventsPath); for await (const line of readLines(events.body)) { const parsed = OpencodeEvent.safeParse(parseJson(line)); if (!parsed.success) { continue; } if (parsed.data.type === "text") { text = parsed.data.part.text; } else { error = parsed.data.error.data?.message ?? parsed.data.error.name; } } if (exitCode === 0) { return { state: "succeeded", result: text }; } if (error === undefined) { const stderr = (await this.readOptionalText(stderrPath)) ?? ""; return { state: "failed", error: `opencode exited with ${exitCode}: ${stderr.slice(-2000)}`, }; } return { state: "failed", error }; } }OpenCode exits with a nonzero code on any model or session error, so the exit code decides the outcome. A successful task returns the last
textevent. A failed task returns the message from the lasterrorevent, such as the message from the gateway for a rejected token or missing Unified Billing credits. -
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 OpenCode.
-
Clone a repository into a sandbox named
opencode-1:curl "$WORKER_URL/sandboxes/opencode-1/repository" \ --json '{"url": "https://github.com/octocat/Hello-World"}' -
Start a task:
curl "$WORKER_URL/sandboxes/opencode-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/opencode-1/task"A
succeededtask includes the last text reply from OpenCode. -
Read the changes:
curl "$WORKER_URL/sandboxes/opencode-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.
- OpenCode example ↗︎: a separate deployable Worker that runs OpenCode.
- OpenCode documentation ↗︎