# Run Claude Code in a sandbox Claude Code is a coding agent that edits files, runs shell commands, and executes whatever it decides your task needs. That is exactly the kind of program you want inside a hardware-isolated VM rather than on your own machine or CI runner: give it a disposable sandbox, let it work with full autonomy, and read back the result. The sandbox is the blast radius. > **What you need** > > An orkestr API token with the sandbox scopes ([quickstart](https://orkestr.eu/docs/sandboxes/quickstart)) and an Anthropic API key for Claude Code itself. The whole flow below runs on the default restricted egress allowlist - npm, GitHub, and `api.anthropic.com` are already on it. ## 1. Create a sandbox Use the `node-22` base and `restricted` networking. Pass the Anthropic key as a sandbox env var - it lives in the VM's memory only and is never persisted. **Python:** ```python from orkestr import Sandbox # node-22 ships Node and npm. restricted egress already covers # registry.npmjs.org, github.com and api.anthropic.com. sbx = Sandbox.create( template="node-22", network="restricted", timeout_seconds=3600, env={"ANTHROPIC_API_KEY": "sk-ant-..."}, ) ``` **JavaScript:** ```javascript import { Sandbox } from "orkestr"; // node-22 ships Node and npm. restricted egress already covers // registry.npmjs.org, github.com and api.anthropic.com. const sbx = await Sandbox.create({ template: "node-22", network: "restricted", timeoutSeconds: 3600, env: { ANTHROPIC_API_KEY: "sk-ant-..." }, }); ``` ## 2. Install Claude Code A plain `npm install` lands in the writable workspace and takes a few seconds. Skip this step entirely by baking the install into a custom template (step 6). **Python:** ```python # A local install lands in the writable workspace - about 4 seconds. sbx.exec("npm install @anthropic-ai/claude-code", timeout_seconds=120) print(sbx.exec("npx claude --version").stdout) # 2.x.x (Claude Code) ``` **JavaScript:** ```javascript // A local install lands in the writable workspace - about 4 seconds. await sbx.exec("npm install @anthropic-ai/claude-code", { timeoutSeconds: 120 }); console.log((await sbx.exec("npx claude --version")).stdout); // 2.x.x (Claude Code) ``` ## 3. Get a repo in **Python:** ```python # github.com is on the default restricted allowlist. sbx.exec("git clone --depth 1 https://github.com/you/your-repo.git repo") # Private repo? Put a fine-grained token in the URL - it stays inside # the sandbox and dies with it: # sbx.exec(f"git clone https://x-access-token:{token}@github.com/you/private.git repo") ``` **JavaScript:** ```javascript // github.com is on the default restricted allowlist. await sbx.exec("git clone --depth 1 https://github.com/you/your-repo.git repo"); // Private repo? Put a fine-grained token in the URL - it stays inside // the sandbox and dies with it: // await sbx.exec(`git clone https://x-access-token:${token}@github.com/you/private.git repo`); ``` ## 4. Run it headless `-p` runs Claude Code non-interactively: one prompt in, the agent works (edits files, runs commands, iterates), one JSON result out. Inside a disposable sandbox, `--dangerously-skip-permissions` is the point rather than a risk - there is nothing of yours here to protect, so the agent never blocks on approval prompts. **Python:** ```python import json result = sbx.exec( 'npx claude -p "Run the test suite and fix the first failing test" ' "--output-format json --dangerously-skip-permissions", cwd="/workspace/repo", timeout_seconds=1800, ) run = json.loads(result.stdout) print(run["result"]) # Claude Code's final answer print(run["total_cost_usd"]) # what the run cost session_id = run["session_id"] # keep it - resumes the conversation later ``` **JavaScript:** ```javascript const result = await sbx.exec( 'npx claude -p "Run the test suite and fix the first failing test" ' + "--output-format json --dangerously-skip-permissions", { cwd: "/workspace/repo", timeoutSeconds: 1800 }, ); const run = JSON.parse(result.stdout); console.log(run.result); // Claude Code's final answer console.log(run.total_cost_usd); // what the run cost const sessionId = run.session_id; // keep it - resumes the conversation later ``` ## 5. Multi-turn sessions Two layers of state compose here. Claude Code's own conversation resumes with `--resume `, and the sandbox around it can be paused between turns so you stop paying for compute while the user thinks - resume brings back the repo, the installed packages, and the agent's session files exactly as they were. **Python:** ```python # Same Claude Code session, next instruction: sbx.exec( f'npx claude -p "Now update the changelog" --resume {session_id} ' "--output-format json --dangerously-skip-permissions", cwd="/workspace/repo", timeout_seconds=1800, ) # Park the whole environment between turns - repo, node_modules, # Claude Code session state, everything: sandbox_id = sbx.pause() # ... later, possibly from another process: sbx = Sandbox.resume(sandbox_id) ``` **JavaScript:** ```javascript // Same Claude Code session, next instruction: await sbx.exec( `npx claude -p "Now update the changelog" --resume ${sessionId} ` + "--output-format json --dangerously-skip-permissions", { cwd: "/workspace/repo", timeoutSeconds: 1800 }, ); // Park the whole environment between turns - repo, node_modules, // Claude Code session state, everything: const sandboxId = await sbx.pause(); // ... later, possibly from another process: const resumed = await Sandbox.resume(sandboxId); ``` ## 6. Make boots instant with a template If you launch Claude Code sandboxes often, bake the install into a [custom template](https://orkestr.eu/docs/sandboxes/templates) once - the install cost moves to build time, and every sandbox booted from the template has `claude` on the PATH in ~300 ms. ```bash curl -X POST https://api.orkestr.eu/v1/templates \ -H "Authorization: Bearer $ORKESTR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "claude-code", "base_template": "node-22", "recipe": ["npm install -g @anthropic-ai/claude-code"], "network": "restricted" }' # Once ready, boot from it - claude is preinstalled, ~300ms to a working agent: # Sandbox.create(template="tmpl_...", network="restricted", env={...}) ``` ## Production notes - **Read files back, not just stdout.** The JSON result carries the agent's answer; the actual work is in the filesystem. Use `sbx.files.read` or `git diff` to collect it, or push a branch from inside the sandbox. - **Budget with timeouts.** Give the sandbox a lifetime that matches the task and each exec its own ceiling - agent runs hang more often than scripts do. - **Scope both credentials.** A sandbox-scoped orkestr token can't touch the rest of your account, and the Anthropic key you inject should be one you can revoke without pain. - **Fan out for parallel work.** Each sandbox is fully isolated - run one Claude Code per candidate fix and keep the branch that passes the tests. See the [cookbook](https://orkestr.eu/docs/sandboxes/cookbook).