Build a coding agent runner·续1

async setUpSandbox() { if (!this.container.running) { this.ctx.storage.kv.delete(taskKey); this.container.start({ image: this.container.images.agent, instance: "standard-1", enableInternet: false, }); }

try { await this.container.interceptAllOutboundHttp(this.ctx.exports.Outbound); await this.container.interceptOutboundHttps( "*", this.ctx.exports.Outbound, ); await this.container.setInactivityTimeout(inactivityTimeout); } catch (error) { // The next request starts a new container. await this.container.destroy(); throw error; } }

async taskStatus() { const exitCode = await this.readOptionalText(exitCodePath);

if (exitCode !== undefined) { return this.outcome(Number.parseInt(exitCode, 10)); }

const pid = await this.readOptionalText(pidPath);

if (pid === undefined) { // The task writes its process ID right after it starts. return this.ctx.storage.kv.get(taskKey) === undefined ? { state: "none" } : { state: "running" }; }

const probe = await this.run( ["/bin/sh", "-c", taskRunningScript, "probe", pidPath], "/", {}, );

if (probe.exitCode === 0) { return { state: "running" }; }

// The task can finish after the first read, so read the exit code // again. const lateExitCode = await this.readOptionalText(exitCodePath);

if (lateExitCode !== undefined) { return this.outcome(Number.parseInt(lateExitCode, 10)); }

return { state: "lost" }; }

async outcome(exitCode) { let result; 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 ?? "" }; }

async readOptionalText(path) { try { return await (await this.files.readFile(path)).text(); } catch (error) { if (SandboxFileError.is(error) && error.code === "ENOENT") { return undefined; }

throw error; } }

async run(command, cwd, env) { const process = await this.container.exec(command, { cwd, env }); const output = await process.output(); const decoder = new TextDecoder();

return { exitCode: output.exitCode, stdout: decoder.decode(output.stdout), stderr: decoder.decode(output.stderr), }; } }

function parseJson(line) { try { return JSON.parse(line); } catch { return undefined; } }

async function* readLines(body) { if (body === null) { return; }

const decoder = new TextDecoder(); let buffered = "";

for await (const chunk of body) { buffered += decoder.decode(chunk, { stream: true }); const lines = buffered.split("\n"); buffered = lines.pop() ?? ""; yield* lines; }

buffered += decoder.decode();

if (buffered !== "") { yield buffered; } }


*src/sandbox.tsts*

```ts
import { Files, SandboxFileError } from "@cloudflare/sandbox";
import { DurableObject } from "cloudflare:workers";
import { z } from "zod";

const repositoryDirectory = "/workspace/repo";
const taskDirectory = "/workspace/task";
const eventsPath = `${taskDirectory}/stdout.log`;
const stderrPath = `${taskDirectory}/stderr.log`;
const exitCodePath = `${taskDirectory}/exit-code`;
const pidPath = `${taskDirectory}/pid`;
const inactivityTimeout = 30 * 60 * 1000;
const taskCheckInterval = 60 * 1000;
const taskKey = "task";

const caPath = "/etc/cloudflare/certs/cloudflare-containers-ca.crt";
// The CA alone replaces the system trust store. This works because
// Outbound intercepts every HTTPS request from the container.
const trustEnv = {
    NODE_EXTRA_CA_CERTS: caPath,
    GIT_SSL_CAINFO: caPath,
    CURL_CA_BUNDLE: caPath,
    SSL_CERT_FILE: caPath,
};

// Runs the command in its own process group. Records its process ID when
// it starts and its exit code when it ends.
const taskScript = `dir=$1; shift
setsid sh -c 'echo "$$ $(cat /proc/sys/kernel/random/boot_id)" >"$0/pid"; exec "$@"' \\
    "$dir" "$@" >"$dir/stdout.log" 2>"$dir/stderr.log"
echo "$?" >"$dir/exit-code.tmp" && mv "$dir/exit-code.tmp" "$dir/exit-code"`;

// Succeeds while the process in the file runs and started in this instance.
const taskRunningScript = `read -r pid boot <"$1" &&
    [ "$boot" = "$(cat /proc/sys/kernel/random/boot_id)" ] &&
    kill -0 "$pid"`;

const ResultEvent = z.object({
    type: z.literal("result"),
    subtype: z.string(),
    is_error: z.boolean(),
    result: z.string().optional(),
});

export type TaskStatus =
    | { state: "none" }
    | { state: "running" }
    | { state: "lost" }
    | { state: "succeeded"; result: string }
    | { state: "failed"; error: string };

export type CommandResult = {
    exitCode: number;
    stdout: string;
    stderr: string;
};

export class AgentSandbox extends DurableObject&lt;Env> {
    private readonly container: Container;
    private readonly files: Files;
    private setup: Promise&lt;void> | undefined;

constructor(ctx: DurableObjectState, env: Env) {
        super(ctx, env);

const container = ctx.container;

if (!container) {
            throw new Error("No container is configured");
        }

this.container = container;
        this.files = new Files(container);

if (container.running) {
            void ctx.blockConcurrencyWhile(() =>
                container.setInactivityTimeout(inactivityTimeout),
            );
        }
    }

async cloneRepository(
        repository: string,
        ref: string | undefined,
    ): Promise&lt;CommandResult> {
        await this.startSandbox();
        const branch = ref === undefined ? [] : ["--branch", ref];

return this.run(
            [
                "git",
                "clone",
                "--depth",
                "1",
                ...branch,
                "--",
                repository,
                repositoryDirectory,
            ],
            "/workspace",
            trustEnv,
        );
    }

startTask(prompt: string): Promise<"started" | "busy"> {
        return this.ctx.blockConcurrencyWhile(async () => {
            await this.startSandbox();

if ((await this.taskStatus()).state === "running") {
                return "busy";
            }

await this.files.remove(taskDirectory, { recursive: true, force: true });
            await this.files.mkdir(taskDirectory);

await this.container.exec(
                [
                    "/bin/sh",
                    "-c",
                    taskScript,
                    "agent",
                    taskDirectory,
                    ...this.agentCommand(prompt),
                ],
                {
                    cwd: repositoryDirectory,
                    env: { ...trustEnv, ...this.agentEnv() },
                    stdout: "ignore",
                    stderr: "ignore",
                },
            );

this.ctx.storage.kv.put(taskKey, "started");
            await this.ctx.storage.setAlarm(Date.now() + taskCheckInterval);
            return "started";
        });
    }

async alarm(): Promise&lt;void> {
        if (!this.container.running) {
            return;
        }

if ((await this.taskStatus()).state === "running") {
            await this.ctx.storage.setAlarm(Date.now() + taskCheckInterval);
            return;
        }

this.ctx.storage.kv.delete(taskKey);
    }

async readTask(): Promise&lt;TaskStatus> {
        if (!this.container.running) {
            return this.ctx.storage.kv.get(taskKey) === undefined
                ? { state: "none" }
                : { state: "lost" };
        }

return this.taskStatus();
    }

async readDiff(): Promise&lt;CommandResult> {
        await this.startSandbox();

return this.run(
            ["/bin/sh", "-c", "git add --intent-to-add . && git diff HEAD"],
            repositoryDirectory,
            {},
        );
    }

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&lt;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",
        };
    }

private async startSandbox(): Promise&lt;void> {
        // Set up each new container, and a running container
        // after this Durable Object restarts.
        if (this.setup === undefined || !this.container.running) {
            this.setup = this.setUpSandbox().catch((error) => {
                this.setup = undefined;
                throw error;
            });
        }

await this.setup;
    }

private async setUpSandbox(): Promise&lt;void> {
        if (!this.container.running) {
            this.ctx.storage.kv.delete(taskKey);
            this.container.start({
                image: this.container.images.agent,
                instance: "standard-1",
                enableInternet: false,
            });
        }

try {
            await this.container.interceptAllOutboundHttp(this.ctx.exports.Outbound);
            await this.container.interceptOutboundHttps(
                "*",
                this.ctx.exports.Outbound,
            );
            await this.container.setInactivityTimeout(inactivityTimeout);
        } catch (error) {
            // The next request starts a new container.
            await this.container.destroy();
            throw error;
        }
    }

private async taskStatus(): Promise&lt;TaskStatus> {
        const exitCode = await this.readOptionalText(exitCodePath);

if (exitCode !== undefined) {
            return this.outcome(Number.parseInt(exitCode, 10));
        }

const pid = await this.readOptionalText(pidPath);

if (pid === undefined) {
            // The task writes its process ID right after it starts.
            return this.ctx.storage.kv.get(taskKey) === undefined
                ? { state: "none" }
                : { state: "running" };
        }

const probe = await this.run(
            ["/bin/sh", "-c", taskRunningScript, "probe", pidPath],
            "/",
            {},
        );

if (probe.exitCode === 0) {
            return { state: "running" };
        }

// The task can finish after the first read, so read the exit code
        // again.
        const lateExitCode = await this.readOptionalText(exitCodePath);

if (lateExitCode !== undefined) {
            return this.outcome(Number.parseInt(lateExitCode, 10));
        }

return { state: "lost" };
    }

private async outcome(exitCode: number): Promise&lt;TaskStatus> {
        let result: z.infer&lt;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 ?? "" };
    }

private async readOptionalText(path: string): Promise&lt;string | undefined> {
        try {
            return await (await this.files.readFile(path)).text();
        } catch (error) {
            if (SandboxFileError.is(error) && error.code === "ENOENT") {
                return undefined;
            }

throw error;
        }
    }

private async run(
        command: string[],
        cwd: string,
        env: Record&lt;string, string>,
    ): Promise&lt;CommandResult> {
        const process = await this.container.exec(command, { cwd, env });
        const output = await process.output();
        const decoder = new TextDecoder();

return {
            exitCode: output.exitCode,
            stdout: decoder.decode(output.stdout),
            stderr: decoder.decode(output.stderr),
        };
    }
}

function parseJson(line: string): unknown {
    try {
        return JSON.parse(line);
    } catch {
        return undefined;
    }
}

async function* readLines(
    body: ReadableStream&lt;Uint8Array> | null,
): AsyncGenerator&lt;string> {
    if (body === null) {
        return;
    }

const decoder = new TextDecoder();
    let buffered = "";

for await (const chunk of body) {
        buffered += decoder.decode(chunk, { stream: true });
        const lines = buffered.split("\n");
        buffered = lines.pop() ?? "";
        yield* lines;
    }

buffered += decoder.decode();

if (buffered !== "") {
        yield buffered;
    }
}

Only ResultEvent, agentCommand(), agentEnv(), and outcome() are specific to Claude Code. The other pages in Coding agents replace those parts to run a different agent in the same Worker.

Start the sandbox

startSandbox() starts the container from the image Wrangler built, with no direct Internet access. In this example, the container uses the standard-1 instance type. Choose a size that fits your agent and your repositories.

After each start, startSandbox() routes HTTP and HTTPS requests from the container on ports 80 and 443 to the Outbound entrypoint. The interception ends with the container, so startSandbox() registers it on every start. For more information, refer to interceptOutboundHttps().

running is true as soon as start() returns, before the interception is registered. A request that arrives during setup waits for the same setUpSandbox() call, so no command runs before the interception is in place. If a setup step fails, setUpSandbox() stops the container, and the next request starts a new one.

A deploy restarts the Durable Object and can stop setUpSandbox() after start(). The restarted Durable Object runs setUpSandbox() again for the container that is already running. Registering an interception again replaces the earlier one, so the container gets its interception before the next command.

Containers re-signs HTTPS traffic from the container with a Cloudflare certificate authority, so the Outbound entrypoint can read it. The trustEnv variables point Git, Node.js, and other tools at that certificate. The Durable Object passes them to every command that makes HTTPS requests. SSL_CERT_FILE, CURL_CA_BUNDLE, and GIT_SSL_CAINFO replace the system trust store with that one certificate, which works only because the Outbound entrypoint receives every HTTPS request. If you copy them to a class that intercepts some hostnames only, add the certificate to the system trust store instead. For more information, refer to Trust the CA certificate.

Run Claude Code in the background

A Claude Code task can run for several minutes, longer than a request should wait. startTask() starts the task and returns. The client then checks the task state with later requests.

Claude Code runs without its own permission prompts, because the sandbox limits what its commands can reach. The command sets these options:

  • --print runs Claude Code without an interactive terminal.
  • --output-format stream-json --verbose writes one JSON event per line, including a final result event.
  • --dangerously-skip-permissions lets Claude Code run commands and edit files without asking. Claude Code requires IS_SANDBOX=1 to allow this as root.
  • ANTHROPIC_BASE_URL sends model requests to the Anthropic endpoint of your gateway.
  • ANTHROPIC_API_KEY is a placeholder. Claude Code does not start without an API key, and the Outbound entrypoint removes the key from each request.
  • CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC turns off update checks, telemetry, and error reporting, so Claude Code calls only your gateway.

If the repository has a CLAUDE.md file, Claude Code reads it, so the instructions in that file apply to the task.

The process writes its output to files instead of sending it back to the Durable Object. A process with piped output receives SIGPIPE after the request that started it ends, which stops Claude Code in the middle of its task.

taskScript is the RUN script from Run background processes. setsid starts Claude Code in its own process group, and the script records the process ID of Claude Code when it starts. When Claude Code exits, the script writes the exit code to a temporary file and then renames it, so the Durable Object never reads a partial exit code.