> ## Documentation Index
> Fetch the complete documentation index at: https://isol8.notdhruv.com/llms.txt
> Use this file to discover all available pages before exploring further.

# One-shot coding agents

> Build your own Stripe Minions — unattended coding agents that clone a repo, implement a task, verify the result, and open a PR — with no human in the loop.

Stripe's [Minions](https://stripe.dev/blog/minions-stripes-one-shot-end-to-end-coding-agents) are unattended coding agents that one-shot tasks end to end: a developer kicks one off, and it produces a complete pull request with no human in the loop. Over 1,300 PRs merge at Stripe each week this way.

This guide shows you how to build the same thing with isol8 — the architecture, each pipeline stage, and the patterns that make one-shot agents reliable.

## Architecture

A one-shot coding agent system has three layers: a **client** that submits tasks, an **orchestrator** that sequences steps, and an **isol8 container** where all code and agent work happens.

The client can be anything — a GitHub bot reacting to issue labels, a Slack bot responding to commands, a web UI with a task form, or a CLI script. It simply sends a task description to the orchestrator and optionally consumes progress events.

```mermaid theme={null}
flowchart TD
  Client["Client — GitHub bot / Slack bot / Web UI / CLI"]
  Orchestrator["Orchestrator — sequences steps, handles retries"]
  Container["isol8 container — persistent mode, shared filesystem"]

  Client -->|"submit task"| Orchestrator
  Orchestrator -->|"execute steps"| Container
  Container -->|"stdout / stderr / exit code"| Orchestrator
  Orchestrator -->|"progress events + PR link"| Client
```

The orchestrator is the core of the system. It creates a single persistent isol8 container and runs a pipeline of steps inside it:

```mermaid theme={null}
flowchart TD
  Setup["Setup — clone repo, install deps"]
  Implement["Implement — agent writes code"]
  Verify["Verify — lint + build"]
  Fix["Fix loop — agent fixes errors"]
  Ship["Ship — commit + open PR"]

  Setup --> Implement --> Verify --> Fix --> Ship
  Fix -->|"still failing"| Fix
```

Every step runs inside the **same container**. The filesystem state built up during setup (`/sandbox/repo`) persists through implement, verify, fix, and ship.

## The container: one persistent session per task

Each task gets a single `DockerIsol8` instance in `mode: "persistent"`. All steps share the container's filesystem.

```typescript theme={null}
const engine = new DockerIsol8({
  mode: "persistent",
  network: "host",
  timeoutMs: 1_800_000,   // 30-minute hard ceiling for the whole run
  memoryLimit: "4g",
  cpuLimit: 2,
  pidsLimit: 200,          // agent spawns subprocesses; default of 64 is too low
  sandboxSize: "4g",
  maxOutputSize: 10 * 1024 * 1024,
  image: "isol8:agent",
  secrets: {
    GITHUB_TOKEN: githubToken,
    ANTHROPIC_API_KEY: anthropicKey,
  },
});

await engine.start();
// ... run all steps ...
await engine.stop(); // always in a finally block
```

<Note>
  `network: "host"` is used here because the agent needs to reach GitHub, the LLM provider API, and package registries simultaneously. If you know your exact set of hostnames, `network: "filtered"` with an explicit whitelist is more secure. See [Security considerations](#security-considerations).
</Note>

## Step 1: Setup — clone before the agent starts

Before the agent receives any prompt, a `setupScript` clones the repo and checks out a branch. Setup scripts run as bash inside the container and complete before the main execution begins.

```typescript theme={null}
function buildSetupScript(repo: string, branch: string): string {
  return `
set -e
git config --global user.email "agent@ci.internal"
git config --global user.name "Agent"
cd /sandbox && rm -rf repo
git clone https://x-access-token:$GITHUB_TOKEN@github.com/${repo}.git repo 2>&1
cd repo
git checkout -b ${branch} || git checkout ${branch}
npm ci
echo "ready — branch ${branch}"`;
}

await engine.execute({
  runtime: "agent",
  setupScript: buildSetupScript("my-org/my-repo", "agent/fix-auth"),
  code: 'echo "[setup] done"',
  timeoutMs: 300_000,
  workdir: "/sandbox/repo",
});
```

<Warning>
  **`git checkout -b` on retry:** With `set -e`, `git checkout -b` exits non-zero if the branch already exists (e.g. on a retry). The `|| git checkout ${branch}` fallback is load-bearing — always include it.
</Warning>

## Step 2: Implement — the agent gets the task

The implement step passes a prompt to the agent runtime. The agent reads files, writes code, and runs tools — all inside the sandbox.

A naive approach passes the raw task description directly:

```typescript theme={null}
await engine.execute({
  runtime: "agent",
  code: `You are working in /sandbox/repo on branch \`${branch}\` of ${repo}.

Implement the following task — read the repo structure first, make all necessary code
changes, run \`npm install\` if node_modules is missing. Do NOT commit anything.

Task:
${task}`,
  agentFlags: "--model anthropic/claude-sonnet-4-5 --no-session",
  timeoutMs: 1_200_000,
  workdir: "/sandbox/repo",
});
```

This works for simple tasks but is fragile. The isol8 agent has no way to ask follow-up questions — the prompt is the complete specification.

<Note>
  **In practice, the orchestrator should act as a master agent.** Gather context before handing off: read relevant files, pull issue details, summarize related PRs, fetch coding guidelines. Construct a self-sufficient prompt that gives the agent everything it needs without clarification.
</Note>

A better implement step looks like this:

```typescript theme={null}
// The orchestrator gathers context before handing off
const relevantFiles = await readFilesFromGitHub(repo, issue.changedPaths);
const relatedIssues = await searchIssues(repo, issue.title);
const codeStyle = await readFile(repo, "CONTRIBUTING.md");

const prompt = `
You are working in /sandbox/repo. The codebase uses TypeScript strict mode.

## Task
${issue.body}

## Files most likely to need changes
${relevantFiles.map(f => `### ${f.path}\n\`\`\`\n${f.content}\n\`\`\``).join("\n\n")}

## Code style rules
${codeStyle}

## Related context
${relatedIssues.map(i => `- #${i.number}: ${i.title}`).join("\n")}

Make all necessary changes. Do NOT commit.
`;

await engine.execute({
  runtime: "agent",
  code: prompt,
  agentFlags: "--model anthropic/claude-sonnet-4-5 --no-session",
  timeoutMs: 1_200_000,
  workdir: "/sandbox/repo",
});
```

## Step 3: Verify — lint and build

After the agent implements, deterministic shell steps verify the result. Lint and build are the only steps allowed to fail — their output is collected and fed into a fix loop, not treated as a hard error.

```typescript theme={null}
const lintResult = await engine.execute({
  runtime: "agent",
  cmd: "npm run lint 2>&1",
  workdir: "/sandbox/repo",
  timeoutMs: 120_000,
});

const buildResult = await engine.execute({
  runtime: "agent",
  cmd: "npm run build 2>&1",
  workdir: "/sandbox/repo",
  timeoutMs: 300_000,
});
```

```mermaid theme={null}
flowchart TD
  Lint["Lint"] --> LintCheck{{"Pass?"}}
  Build["Build"] --> BuildCheck{{"Pass?"}}
  LintCheck -->|Yes| BuildCheck
  LintCheck -->|No| CollectErrors["Collect errors"]
  BuildCheck -->|No| CollectErrors
  BuildCheck -->|Yes| Ship["Ship — commit + PR"]
  CollectErrors --> FixLoop["Fix loop"]
```

## Step 4: Fix loop — automated retry

If lint or build fails, the fix loop runs the agent again with the error output, then re-verifies. Two rounds is the practical ceiling — after that, hand off to humans.

```typescript theme={null}
const maxFixRounds = 2;

for (let round = 1; round <= maxFixRounds; round++) {
  await engine.execute({
    runtime: "agent",
    code: `You are working in /sandbox/repo. Fix all of the following errors.
Do NOT commit anything.

${lintErrors}
${buildErrors}`,
    agentFlags: "--model anthropic/claude-sonnet-4-5 --no-session",
    timeoutMs: 900_000,
    workdir: "/sandbox/repo",
  });

  // Re-verify after fix
  const relint = await engine.execute({
    runtime: "agent",
    cmd: "npm run lint 2>&1",
    workdir: "/sandbox/repo",
    timeoutMs: 120_000,
  });
  const rebuild = await engine.execute({
    runtime: "agent",
    cmd: "npm run build 2>&1",
    workdir: "/sandbox/repo",
    timeoutMs: 300_000,
  });

  if (relint.exitCode === 0 && rebuild.exitCode === 0) break;
  if (round === maxFixRounds) throw new Error("Still failing after max fix rounds");
}
```

<Tip>
  Diminishing returns set in quickly. Two fix rounds is the practical ceiling — after that, hand off to humans rather than burning more tokens.
</Tip>

## Step 5: Ship — commit and open a PR

The commit and PR steps are also delegated to the agent. Two shell patterns matter:

```typescript theme={null}
// Commit — write message to a file, use -F not -m (avoids interactive editor traps)
await engine.execute({
  runtime: "agent",
  code: `Write a conventional commit message to /tmp/commit-msg.txt, then run:
git add -A && git commit -F /tmp/commit-msg.txt && git push -u origin ${branch}`,
  agentFlags: "--model anthropic/claude-sonnet-4-5 --no-session",
  timeoutMs: 300_000,
  workdir: "/sandbox/repo",
});

// PR — write body to a file, use --body-file not --body (avoids shell escaping issues)
const prResult = await engine.execute({
  runtime: "agent",
  code: `Write the PR title to /tmp/pr-title.txt and body to /tmp/pr-body.md, then run:
gh pr create --title "$(cat /tmp/pr-title.txt)" --body-file /tmp/pr-body.md --base main --head ${branch}
Output the resulting PR URL on its own line.`,
  agentFlags: "--model anthropic/claude-sonnet-4-5 --no-session",
  timeoutMs: 300_000,
  workdir: "/sandbox/repo",
});

// Extract the PR URL from output
const PR_URL_REGEX = /https:\/\/github\.com\/[^\s]+\/pull\/(\d+)/;
const prMatch = (prResult.stdout + prResult.stderr).match(PR_URL_REGEX);
const prUrl = prMatch?.[0];
```

## Full pipeline

Putting it all together, the orchestrator function:

```typescript theme={null}
async function runOneShot(task: string, repo: string, branch: string) {
  const engine = new DockerIsol8({
    mode: "persistent",
    network: "host",
    timeoutMs: 1_800_000,
    memoryLimit: "4g",
    cpuLimit: 2,
    pidsLimit: 200,
    sandboxSize: "4g",
    image: "isol8:agent",
    secrets: {
      GITHUB_TOKEN: process.env.GITHUB_TOKEN!,
      ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY!,
    },
  });

  await engine.start();

  try {
    // 1. Setup
    await engine.execute({
      runtime: "agent",
      setupScript: buildSetupScript(repo, branch),
      code: 'echo "setup complete"',
      timeoutMs: 300_000,
    });

    // 2. Implement
    await engine.execute({
      runtime: "agent",
      code: buildPrompt(task, repo, branch),
      agentFlags: "--model anthropic/claude-sonnet-4-5 --no-session",
      timeoutMs: 1_200_000,
      workdir: "/sandbox/repo",
    });

    // 3. Verify
    const lint = await engine.execute({
      runtime: "agent",
      cmd: "npm run lint 2>&1",
      workdir: "/sandbox/repo",
      timeoutMs: 120_000,
    });
    const build = await engine.execute({
      runtime: "agent",
      cmd: "npm run build 2>&1",
      workdir: "/sandbox/repo",
      timeoutMs: 300_000,
    });

    // 4. Fix loop (if needed)
    if (lint.exitCode !== 0 || build.exitCode !== 0) {
      await runFixLoop(engine, lint, build, branch);
    }

    // 5. Ship
    const prUrl = await commitAndCreatePR(engine, branch);
    return prUrl;
  } finally {
    await engine.stop();
  }
}
```

## Streaming progress to clients

Every step can use `executeStream()` to emit progress in real-time. The `phase` field on each `StreamEvent` distinguishes setup output from agent output:

```typescript theme={null}
for await (const event of engine.executeStream({
  runtime: "agent",
  setupScript: buildSetupScript(repo, branch),
  code: buildPrompt(task, repo, branch),
  agentFlags: "--model anthropic/claude-sonnet-4-5 --no-session",
})) {
  if (event.phase === "setup") {
    onProgress({ step: "setup", data: event.data });
  } else if (event.type === "stdout") {
    onProgress({ step: "implement", data: event.data });
  } else if (event.type === "exit") {
    onProgress({ step: "implement", data: `exited: ${event.data}` });
  }
}
```

How you relay these events to the client depends on your architecture — SSE, WebSockets, a message queue, or writing to a database that the client polls. The orchestrator produces `StreamEvent`s; what the client does with them is up to you.

<Note>
  If the `setupScript` exits non-zero, the stream yields a `{ type: "error", phase: "setup" }` event followed by an `exit` event, and the agent never starts. Filter on `phase` to surface setup failures separately from agent failures.
</Note>

## Concurrency and cancellation

When running multiple tasks concurrently, use a job queue with bounded concurrency. Each task should get its own `AbortController` for cancellation — aborting triggers `engine.stop()` to destroy the container immediately.

```typescript theme={null}
import PQueue from "p-queue";

const queue = new PQueue({ concurrency: 3 });
const controllers = new Map<string, AbortController>();

function enqueueTask(taskId: string, task: string, repo: string, branch: string) {
  const controller = new AbortController();
  controllers.set(taskId, controller);

  queue.add(async () => {
    if (controller.signal.aborted) return;
    await runOneShot(task, repo, branch);
  }, { signal: controller.signal });
}

function cancelTask(taskId: string) {
  controllers.get(taskId)?.abort();
}
```

Inside the orchestrator, wire the abort signal to `engine.stop()`:

```typescript theme={null}
const onAbort = () => engine.stop().catch(() => undefined);
signal?.addEventListener("abort", onAbort, { once: true });

try {
  // ... all steps ...
} finally {
  signal?.removeEventListener("abort", onAbort);
  await engine.stop();
}
```

## Security considerations

For production deployments — especially multi-tenant or untrusted-task workloads — use `network: "filtered"` with an explicit allowlist instead of `network: "host"`:

```typescript theme={null}
const engine = new DockerIsol8({
  mode: "persistent",
  network: "filtered",
  networkFilter: {
    whitelist: [
      "^api\\.anthropic\\.com$",       // LLM API
      "^github\\.com$",                // git clone + push
      "^api\\.github\\.com$",          // gh CLI (pr create, auth)
      "^registry\\.npmjs\\.org$",      // npm installs
    ],
    blacklist: [
      "^169\\.254\\.",                  // link-local (cloud metadata)
      "^10\\.",                         // RFC 1918
      "^172\\.(1[6-9]|2[0-9]|3[01])\\.", // RFC 1918
    ],
  },
  secrets: {
    GITHUB_TOKEN: githubToken,
    ANTHROPIC_API_KEY: anthropicKey,
  },
  memoryLimit: "4g",
  cpuLimit: 2,
  pidsLimit: 200,
  maxOutputSize: 10 * 1024 * 1024,
});
```

All other isol8 isolation guarantees apply regardless of network mode: read-only root filesystem, non-root `sandbox` user, seccomp syscall filtering, `/sandbox` tmpfs, and automatic secret masking in output.

## Related pages

<CardGroup cols={2}>
  <Card title="Agent in a Box" icon="microchip-ai" href="/agent-in-a-box">
    Full reference for the agent runtime: flags, networking, file injection, and streaming.
  </Card>

  <Card title="Setup scripts" icon="scroll" href="/setup-scripts">
    Image-level vs request-level setup, execution order, streaming output, and error handling.
  </Card>

  <Card title="AI agent code execution" icon="robot" href="/guides/ai-agents">
    Foundational patterns for LLM tool-call loops with isol8.
  </Card>

  <Card title="Security model" icon="shield-check" href="/security">
    Network controls, seccomp, secret masking, and isolation boundaries.
  </Card>

  <Card title="Remote server" icon="server" href="/remote">
    Deploy isol8 as a centralized execution server for agent fleets.
  </Card>
</CardGroup>
