Documentation

Install, define a sandbox, and drive it from the CLI or from code.

Install

box needs Linux with KVM (or macOS with a podman machine), podman, and crun built with libkrun. The firecracker backend downloads a pinned Firecracker and guest kernel on first use and verifies their SHA-256.

curl -fsSL https://chxperiments.github.io/box/install.sh | sh
box doctor          # checks podman, KVM, krun, subordinate UIDs; prints a fix for each failure

Quick start

box new agent --from tiny-python    # a ~60 MB Python sandbox
box build agent                     # builds, then proves it has its own kernel
box run agent -- python3 -c 'print(6*7)'

box up agent                        # keep one VM running
box exec agent -- pip list
box down agent

For agents, add isolation: strict to the Boxfile. The tiny-python, tiny-node and ai-agent examples already have it.

Boxfile

One YAML file per sandbox at ~/.box/sandboxes/<name>/Boxfile. Unknown keys are rejected, so a typo fails loudly.

FieldValuesDefaultMeaning
baseimage referencealpine:latestThe image every VM starts from.
backendpodman, krun, firecrackerpodmanWhat boots the VM. See Architecture.
isolationstandard, strictstandardstrict runs the VMM as a subordinate UID.
cpus1 to 162vCPUs.
ram_mib128 and up2048Guest RAM in MiB.
networkbridge, nonebridgeInternet access, or none at all.
readonlyboolfalseRead-only root; /tmp and /data stay writable.
timeout_secondsint0Per-command limit; exits 124. 0 is unlimited.
warm0 to 80VMs kept booted for run (podman, krun).
warmupshell linesRun once in each warm VM before it serves a run.
packagesnamesInstalled with the image's package manager.
runshell linesExtra build steps.
envmapEnvironment for every command.
mountshost, guest, modeHost directories shared in (podman, krun); ro by default.
blueprintusers, write_files, runcmdProvisioning applied at build time.
pkgmgrapk, apt, dnfinferredOverride the package manager.
passtboolfalseA real NIC with a default route (podman, krun).
seccomppathA seccomp profile for the VMM.

CLI

CommandWhat it does
box new <name> [--from example]Create a sandbox, optionally from a shipped example
box build <name>Build the image and prove the sandbox has its own kernel
box run <name> -- <cmd>Run one command in a fresh VM
box up / down <name>Keep a VM running, or stop it
box exec <name> -- <cmd>Run in the running VM; state carries over
box shell <name>Interactive session
box fork <name> <fork>Branch /data
box diff / apply / discard <fork>Review, merge or drop a fork's changes
box data import / export <name> <dir>Move files into or out of /data
box snapshot / restore <name> [label]Archive /data and roll back
box reset <name>Empty /data
box serveThe local API the SDKs use
box doctorCheck the host setup, with a fix for each failure
box ls, env, logs, verifyInspect sandboxes
box rename, destroy, nukeRemove or rename

SDKs

Python, TypeScript, Go and Rust. Each talks to box serve over a Unix socket only your user can open, and starts it when nothing is listening. Pick a language once; every sample on this page follows.

pip install ./sdk/python          # from a box checkout

from sdbox import Client, Sandbox

up and down

Boot a VM once and keep it running for exec. The context-manager forms bring it down afterwards unless it was already up.

sb = Sandbox("agent")
sb.up()
...
sb.down()

# or
with Sandbox("agent") as sb:
    ...

exec and run

exec runs in the running VM, so state carries over. run takes a fresh VM and throws it away. A command is a shell string or an argv list; a timeout exits 124.

r = sb.exec(["python3", "-c", "print(6*7)"])
r = sb.exec("ls -la | head", timeout=10)
r = sb.exec("wc -c", stdin=b"bytes")

r = Sandbox("agent").run("pytest -q")

Files

Reads and writes happen inside the guest, so a path or symlink the sandbox controls can never redirect them onto a host file. For bulk moves, use box data import and export.

sb.write_file("/data/task.py", "print(1)")
data = sb.read_file("/data/result.json")

Forks: fork, diff, apply, discard

A fork has its parent's image and a /data overlaid on the parent's. Running it never touches the parent. diff lists changes as A added, M modified, D deleted; apply merges them into the parent; discard drops them. Apply is refused while either side is up.

trial = Sandbox("agent").fork("trial")
trial.run("python3 refactor.py").check()

for ch in trial.diff():
    print(ch.kind, ch.path)

if tests_pass:
    trial.apply()       # returns how many changes were merged
else:
    trial.discard()

Results and errors

A non-zero exit is a result, not an exception. Errors carry a machine-readable code: no_sandbox, not_up, bad_name, bad_request, no_server.

r = sb.exec("make test")
r.exit_code, r.stdout, r.stderr, r.duration_ms, r.timed_out
r.ok; r.stdout_text
r.check()               # raises CommandFailed on a non-zero exit

from sdbox import NotFound, NotUp, BoxError

MCP for agents

box mcp serves box over the Model Context Protocol on stdin and stdout, so an agent in Claude Code, Claude Desktop, Cursor or any MCP client can run code in your sandboxes. Every tool goes through the same handler as the local API, with the same checks. Sandboxes are still created and built by you, on the CLI.

claude mcp add box -- box mcp
{ "mcpServers": { "box": { "command": "box", "args": ["mcp"] } } }
ToolWhat it does
list_sandboxesThe sandboxes the agent may use
runA command in a fresh VM
up / exec / downKeep a VM and run commands in it, state carried over
read_file / write_fileFile I/O inside the guest
fork / diff / discardBranch /data, review the changes, drop them

apply is not offered to agents unless you start the server with --allow-apply. A fork exists so that you review an agent's work before it reaches real data; an agent that could apply its own fork would skip that.

Local API

HTTP with JSON bodies on ~/.box/box.sock, owner-only. stdout, stderr and stdin are base64. The SDKs are thin clients of this.

MethodPathBody or result
GET/v1/version
GET/v1/sandboxesname, up, warm, warm_target, network, base
POST/v1/sandboxes/{name}/up
POST/v1/sandboxes/{name}/down
POST/v1/sandboxes/{name}/execargv, stdin (base64), timeout_seconds
POST/v1/sandboxes/{name}/runsame, in a fresh VM
POST/v1/sandboxes/{name}/forkas
GET/v1/sandboxes/{name}/diffreturns changes: [kind, path]
POST/v1/sandboxes/{name}/applyreturns changes: n
POST/v1/sandboxes/{name}/discardreturns changes: n