Run a full coding agent — pi — inside an isol8 sandbox. Give an LLM a filesystem, tools, and controlled network access in a single execute() call.
The agent runtime runs the pi coding agent (@mariozechner/pi-coding-agent) inside an isol8 container. Instead of executing code, it executes a prompt — pi handles the LLM loop, tool calls (read, write, edit, bash), and file edits autonomously, entirely within the sandbox.
pi --no-session --append-system-prompt '<sandbox context>' [agentFlags] -p '<code>'
--no-session — disables session persistence (ephemeral, non-interactive). This is always set by isol8; you do not need to include it in agentFlags.
--append-system-prompt — automatically injected by isol8 to inform pi of sandbox constraints
agentFlags — extra pi flags you supply (model, thinking level, tool restrictions)
-p '<code>' — your prompt, shell-quoted
pi then runs its own tool-call loop inside the container. It can read, write, and edit files under /sandbox, and run arbitrary bash commands — all within the sandbox’s resource and network limits.
The isol8:agent Docker image (which provides bun, pi, and gh) is built automatically when you run isol8 setup or when DockerIsol8 first uses the agent runtime. If you need to build it manually — for example in an offline environment — run:
"filtered" requires at least one whitelist entry — an empty whitelist throws:
Error: Agent runtime requires at least one network whitelist entry.
const engine = new DockerIsol8({ network: "host",});
"host" gives the agent full access to your host network. Use only in trusted, controlled environments. Prefer "filtered" with an explicit whitelist whenever possible.
Every pi invocation inside isol8 receives an automatically appended system prompt informing the agent that it is running in a sandbox with restricted network access and an ephemeral filesystem. This uses pi’s --append-system-prompt — it appends to pi’s default prompt without replacing it. You do not need to supply this yourself.
Use files in ExecutionRequest (library/API) or --files <dir> (CLI) to inject local files into /sandbox before the agent runs.
Library
CLI
import { readFileSync } from "node:fs";await engine.execute({ runtime: "agent", code: "Review the code in /sandbox and suggest improvements to error handling", agentFlags: "--model anthropic/claude-sonnet-4-5 --tools read,bash", files: { "src/auth.ts": readFileSync("./src/auth.ts", "utf-8"), "src/utils.ts": readFileSync("./src/utils.ts", "utf-8"), // pi auto-loads AGENTS.md from cwd — use this for project rules "AGENTS.md": "# Rules\n- Follow existing code style\n- No new dependencies\n", },});
# Inject an entire local directory into /sandboxisol8 run -e "Review the code and suggest improvements" \ --runtime agent \ --files ./src \ --net filtered \ --allow "api.anthropic.com"
pi automatically loads AGENTS.md (and CLAUDE.md) from the working directory at startup. Injecting your project rules as /sandbox/AGENTS.md gives the agent project-specific context without touching the prompt.
A setupScript runs as a bash script inside the container before pi receives its prompt. Use it to clone repos, write config files, install tools, or prepare any state the agent needs. The script runs as the sandbox user from /sandbox.
The agent may need authenticated access to npm or private git remotes. Write config files via the setup script so credentials are in place before pi starts:
await engine.execute({ runtime: "agent", setupScript: ` # Authenticate npm to private registry cat > /sandbox/.npmrc << 'EOF'registry=https://registry.npmjs.org///registry.npmjs.org/:_authToken=$NPM_TOKENEOF # Configure git identity for commits git config --global user.name "isol8-agent" git config --global user.email "agent@ci.internal" # Clone the target repo git clone https://$GITHUB_TOKEN@github.com/my-org/my-repo.git /sandbox/repo cd /sandbox/repo && npm ci `, code: "Add end-to-end tests for the checkout flow. Use the existing test patterns in tests/e2e/.", agentFlags: "--model anthropic/claude-sonnet-4-5 --thinking low", timeoutMs: 600_000,});
pi auto-loads AGENTS.md from its working directory. Write project rules via the setup script to give the agent context without touching the prompt:
await engine.execute({ runtime: "agent", setupScript: ` git clone https://$GITHUB_TOKEN@github.com/my-org/my-repo.git /sandbox/repo # Write project rules — pi picks these up automatically cat > /sandbox/repo/AGENTS.md << 'EOF'# Coding rules- Follow existing code style- No new runtime dependencies without approval- All new functions must have JSDoc comments- Tests live in tests/ — use vitestEOF `, code: "Refactor the authentication module to use async/await throughout.", agentFlags: "--model anthropic/claude-sonnet-4-5", timeoutMs: 300_000,});
For setup that never changes between runs (git identity, tool config, registry auth), bake it into a custom image using prebuiltImages[].setupScript in your config. The script runs on every execution against that image without adding per-request latency:
pi produces output incrementally. Use executeStream to receive it in real-time:
for await (const event of engine.executeStream({ runtime: "agent", code: "Refactor the auth module to remove deprecated API calls", agentFlags: "--model anthropic/claude-sonnet-4-5",})) { if (event.type === "stdout") process.stdout.write(event.data); if (event.type === "stderr") process.stderr.write(event.data); if (event.type === "exit") console.log(`\nAgent exited: ${event.data}`);}
Each event carries an optional phase field ("setup" or "code") so you can distinguish setup-script output from agent output:
for await (const event of engine.executeStream({ runtime: "agent", setupScript: "git clone https://$GITHUB_TOKEN@github.com/my-org/repo.git /sandbox/repo", code: "Fix the type errors in src/parser.ts", agentFlags: "--model anthropic/claude-sonnet-4-5",})) { if (event.phase === "setup") { // output from the setupScript (clone, config, etc.) process.stderr.write(`[setup] ${event.data}`); } else if (event.type === "stdout") { process.stdout.write(event.data); } else if (event.type === "exit") { console.log(`\nAgent exited: ${event.data}`); }}
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.
The agent runtime spawns subprocesses for tool calls (bash, package installs, git operations). The default pidsLimit of 64 is often too low — explicitly set pidsLimit: 200 to avoid process limit errors:
const engine = new DockerIsol8({ network: "filtered", networkFilter: { whitelist: ["^api\\.anthropic\\.com$"], blacklist: [] }, pidsLimit: 200, // required — the default of 64 is too low for agent workloads});
Use outputPaths to include files written by the agent in the result:
const result = await engine.execute({ runtime: "agent", code: "Generate a test suite for the Parser class and write it to /sandbox/parser.test.ts", outputPaths: ["/sandbox/parser.test.ts"],});console.log(result.files?.["/sandbox/parser.test.ts"]);
Or retrieve files explicitly with getFile() after execution in a persistent session.
Error: Agent runtime requires network access — Switch to network: "filtered" with at least one whitelist entry, or network: "host". network: "none" is not supported for the agent runtime.Agent exits non-zero — Check result.stderr. Common causes: missing API key, endpoint not in whitelist, timeoutMs too short.Agent can’t reach the LLM API — Verify the whitelist pattern. Patterns are matched as extended regular expressions using grep -E (substring match, not full-string). Without anchors, a pattern like anthropic\\.com would also match evil-anthropic.com.attacker.net. Use ^ and $ anchors for precise matching: ^api\\.anthropic\\.com$.Files not in result — Add outputPaths or call getFile() after the run. In ephemeral mode, container state is discarded on exit.