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 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.
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.The orchestrator is the core of the system. It creates a single persistent isol8 container and runs a pipeline of steps inside it:Every step runs inside the same container. The filesystem state built up during setup (/sandbox/repo) persists through implement, verify, fix, and ship.
Each task gets a single DockerIsol8 instance in mode: "persistent". All steps share the container’s filesystem.
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
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.
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.
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.
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:
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 codechanges, 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.
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.
A better implement step looks like this:
// The orchestrator gathers context before handing offconst 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",});
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.
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.
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");}
Diminishing returns set in quickly. Two fix rounds is the practical ceiling — after that, hand off to humans rather than burning more tokens.
The commit and PR steps are also delegated to the agent. Two shell patterns matter:
// 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 outputconst PR_URL_REGEX = /https:\/\/github\.com\/[^\s]+\/pull\/(\d+)/;const prMatch = (prResult.stdout + prResult.stderr).match(PR_URL_REGEX);const prUrl = prMatch?.[0];
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 StreamEvents; what the client does with them is up to you.
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.
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.
For production deployments — especially multi-tenant or untrusted-task workloads — use network: "filtered" with an explicit allowlist instead of network: "host":