NAME
zerobox — Lightweight, cross-platform process sandboxing powered by OpenAI Codex's runtime. Sandbox any command with file,…
SYNOPSIS
npm install -g zeroboxINFO
DESCRIPTION
Lightweight, cross-platform process sandboxing powered by OpenAI Codex's runtime. Sandbox any command with file, network, and credential controls.
README
Lightweight, cross-platform process sandboxing powered by OpenAI Codex's sandbox runtime.
- Deny by default: Writes, network, and environment variables are blocked unless you allow them
- Credential injection: Pass API keys that the process never sees. Zerobox injects real values only for approved hosts
- File access control: Allow or deny reads and writes to specific paths
- Network filtering: Allow or deny outbound traffic by domain
- Clean environment: Only essential env vars (PATH, HOME, etc.) are inherited by default
- Rust SDK:
use zerobox::Sandboxwith a builder API - TypeScript SDK:
import { Sandbox } from "zerobox"with a Deno-style API - Cross-platform: macOS and Linux. Windows support planned
- Single binary: No Docker, no VMs, ~10ms overhead
Install
Shell (macOS / Linux)
curl -fsSL https://raw.githubusercontent.com/afshinm/zerobox/main/install.sh | sh
npm
npm install -g zerobox
From source
git clone https://github.com/afshinm/zerobox && cd zerobox
./scripts/sync.sh && cargo build --release -p zerobox
Quick start
Run a command with no writes and no network access:
zerobox -- node -e "console.log('hello')"
Allow writes to a specific directory:
zerobox --allow-write=. -- node script.js
Allow network to a specific domain:
zerobox --allow-net=api.openai.com -- node agent.js
Pass a secret to a specific host and the inner process never sees the real value:
zerobox --secret OPENAI_API_KEY=sk-proj-123 --secret-host OPENAI_API_KEY=api.openai.com -- node agent.js
Same thing with the Rust SDK:
use zerobox::Sandbox;
let output = Sandbox::command("node") .arg("agent.js") .secret("OPENAI_API_KEY", "sk-proj-123") .secret_host("OPENAI_API_KEY", "api.openai.com") .run() .await?;
Or the TypeScript SDK:
import { Sandbox } from "zerobox";const sandbox = Sandbox.create({ secrets: { OPENAI_API_KEY: { value: process.env.OPENAI_API_KEY, hosts: ["api.openai.com"], }, }, });
const output = await sandbox.shnode agent.js.text();
Record filesystem changes and undo them after execution:
zerobox --restore --allow-write=. -- npm install
Or record without restoring, then inspect and undo later:
zerobox --snapshot --allow-write=. -- npm install
zerobox snapshot list
zerobox snapshot diff <session-id>
zerobox snapshot restore <session-id>
Architecture
Secrets
Secrets are API keys, tokens, or credentials that should never be visible inside the sandbox. The sandboxed process sees a placeholder in the environment variable and the real value is substituted at the network proxy level only for requested hosts:
sandbox process: echo $OPENAI_API_KEY -> ZEROBOX_SECRET_a1b2c3d4e5... (placeholder)
sandbox process: curl -H "Authorization: Bearer $OPENAI_API_KEY" https://api.openai.com/... -> proxy intercepts, replaces placeholder with real key -> server receives: Authorization: Bearer sk-proj-123
Using the CLI
Pass a secret with --secret and restrict it to a specific domain with --secret-host:
zerobox --secret OPENAI_API_KEY=sk-proj-123 --secret-host OPENAI_API_KEY=api.openai.com -- node app.js
Without --secret-host, the secret is passed to all domains:
zerobox --secret TOKEN=abc123 -- node app.js
You can also pass multiple secrets with different domains:
zerobox \
--secret OPENAI_API_KEY=sk-proj-123 --secret-host OPENAI_API_KEY=api.openai.com \
--secret GITHUB_TOKEN=ghp-456 --secret-host GITHUB_TOKEN=api.github.com \
-- node app.js
Node.js
fetchdoes not respectHTTPS_PROXYby default. When running Node.js inside a sandbox with secrets, make sure to pass the--use-env-proxyargument.
Rust SDK
let output = Sandbox::command("node")
.arg("agent.js")
.secret("OPENAI_API_KEY", "sk-proj-123")
.secret_host("OPENAI_API_KEY", "api.openai.com")
.secret("GITHUB_TOKEN", "ghp-456")
.secret_host("GITHUB_TOKEN", "api.github.com")
.run()
.await?;
TypeScript SDK
import { Sandbox } from "zerobox";const sandbox = Sandbox.create({ secrets: { OPENAI_API_KEY: { value: process.env.OPENAI_API_KEY, hosts: ["api.openai.com"], }, GITHUB_TOKEN: { value: process.env.GITHUB_TOKEN, hosts: ["api.github.com"], }, }, });
await sandbox.shnode agent.js.text();
Environment variables
By default, only essential variables are passed to the sandbox e.g. PATH, HOME, USER, SHELL, TERM, LANG.
Inherit all parent env vars
The --allow-env flag allows all parent environment variables to be inherited by the sandboxed process:
zerobox --allow-env -- node app.js
Inherit specific env vars only
zerobox --allow-env=PATH,HOME,DATABASE_URL -- node app.js
Block specific env vars
zerobox --allow-env --deny-env=AWS_SECRET_ACCESS_KEY -- node app.js
or set a specific variable:
zerobox --env NODE_ENV=production --env DEBUG=false -- node app.js
Rust SDK
let output = Sandbox::command("node")
.arg("app.js")
.env("NODE_ENV", "production")
.allow_env(&["PATH", "HOME"])
.deny_env(&["AWS_SECRET_ACCESS_KEY"])
.run()
.await?;
TypeScript SDK
const sandbox = Sandbox.create({
env: { NODE_ENV: "production" },
allowEnv: ["PATH", "HOME"],
denyEnv: ["AWS_SECRET_ACCESS_KEY"],
});
Examples
Run AI-generated code safely
Run AI generated code without risking file corruption or data leaks:
zerobox -- python3 /tmp/task.py
Or allow writes only to an output directory:
zerobox --allow-write=/tmp/output -- python3 /tmp/task.py
Or via the Rust SDK:
let output = Sandbox::command("python3") .arg("/tmp/task.py") .allow_write("/tmp/output") .allow_net(&["api.openai.com"]) .run() .await?;
println!("{}", String::from_utf8_lossy(&output.stdout));
Or the TypeScript SDK:
import { Sandbox } from "zerobox";const sandbox = Sandbox.create({ allowWrite: ["/tmp/output"], allowNet: ["api.openai.com"], });
const result = await sandbox.shpython3 /tmp/task.py.output(); console.log(result.code, result.stdout);
Restrict LLM tool calls
Each AI tool call can also be sandboxed individually. The parent agent process runs normally and only some operations are sandboxed:
import { Sandbox } from "zerobox";const reader = Sandbox.create(); const writer = Sandbox.create({ allowWrite: ["/tmp"] }); const fetcher = Sandbox.create({ allowNet: ["example.com"] });
const data = await reader.js
const content = require("fs").readFileSync("/tmp/input.txt", "utf8"); console.log(JSON.stringify({ content }));.json();await writer.js
require("fs").writeFileSync("/tmp/output.txt", "result"); console.log("ok");.text();
const result = await fetcher.jsconst res = await fetch("https://example.com"); console.log(JSON.stringify({ status: res.status }));.json();
Full working examples:
examples/ai-agent-sandboxed- Entire agent process sandboxed with secrets (API key never visible)examples/ai-agent- Vercel AI SDK with per-tool sandboxing and secretsexamples/workflow- Vercel Workflow with sandboxed durable steps
Protect your repo during builds
Run a build script with network access:
zerobox --allow-write=./dist --allow-net -- npm run build
Run tests with no network and catch accidental external calls:
zerobox --allow-write=/tmp -- npm test
Rust SDK
[dependencies]
zerobox = "0.1"
Run and collect output
use zerobox::Sandbox;let output = Sandbox::command("echo") .arg("hello") .allow_write("/tmp") .run() .await?;
println!("{}", String::from_utf8_lossy(&output.stdout)); println!("exit: {}", output.status);
Stream output
let mut child = Sandbox::command("cargo") .arg("build") .allow_write("/project/target") .allow_net(&["crates.io"]) .spawn() .await?;
let stdout = child.stdout().unwrap(); // read from stdout while the process runs let status = child.wait().await?;
Inherit stdio (TTY passthrough)
let status = Sandbox::command("vim")
.allow_write("/project")
.status()
.await?;
Profiles
// default profile loads automatically (denies ~/.ssh, ~/.aws, etc.) let output = Sandbox::command("npm test").run().await?;// use a different profile let output = Sandbox::command("npm test") .profile("workspace") .run() .await?;
// opt out of profiles let output = Sandbox::command("npm test") .no_profile() .allow_read("/src") .run() .await?;
Full access / no sandbox
let output = Sandbox::command("install.sh") .full_access() .run() .await?;
let output = Sandbox::command("ls") .no_sandbox() .run() .await?;
TypeScript SDK
npm install zerobox
Shell commands
import { Sandbox } from "zerobox";
const sandbox = Sandbox.create({ allowWrite: ["/tmp"] }); const output = await sandbox.shecho hello.text();
JSON output
const data = await sandbox.sh`cat data.json`.json();
Raw output (doesn't throw on non-zero exit)
const result = await sandbox.sh`exit 42`.output();
// { code: 42, stdout: "", stderr: "" }
Explicit command + args
await sandbox.exec("node", ["-e", "console.log('hi')"]).text();
Inline JavaScript
const data = await sandbox.js`
console.log(JSON.stringify({ sum: 1 + 2 }));
`.json();
Error handling
Non-zero exit codes throw SandboxCommandError:
import { Sandbox, SandboxCommandError } from "zerobox";
const sandbox = Sandbox.create(); try { await sandbox.shexit 1.text(); } catch (e) { if (e instanceof SandboxCommandError) { console.log(e.code); // 1 console.log(e.stderr); } }
Snapshots
const sandbox = Sandbox.create({ allowWrite: ["."], restore: true, });
// Changes are automatically undone after execution. await sandbox.shnpm install.text();
Record without restoring:
const sandbox = Sandbox.create({ allowWrite: ["."], snapshot: true, snapshotExclude: ["node_modules"], });
await sandbox.shnpm install.text();
Cancellation
const controller = new AbortController();
await sandbox.sh`sleep 60`.text({ signal: controller.signal });
Performance
Sandbox overhead is minimal, typically ~10ms and ~7MB:
| Command | Bare | Sandboxed | Overhead | Bare Mem | Sandbox Mem |
|---|---|---|---|---|---|
echo hello | <1ms | 10ms | +10ms | 1.2 MB | 8.4 MB |
node -e '...' | 10ms | 20ms | +10ms | 39.3 MB | 39.1 MB |
python3 -c '...' | 10ms | 20ms | +10ms | 12.9 MB | 13.0 MB |
cat 10MB file | <1ms | 10ms | +10ms | 1.9 MB | 8.4 MB |
curl https://... | 50ms | 60ms | +10ms | 7.2 MB | 8.4 MB |
Best of 10 runs with warmup on Apple M5 Pro. Run ./bench/run.sh to reproduce.
Platform support
| Platform | Backend | Status |
|---|---|---|
| macOS | Seatbelt (sandbox-exec) | Fully supported |
| Linux | Bubblewrap + Seccomp + Namespaces | Fully supported |
| Windows | Restricted Tokens + ACLs + Firewall | Planned |
CLI reference
| Flag | Example | Description |
|---|---|---|
--allow-read <paths> | --allow-read=/tmp,/data | Restrict readable user data to listed paths. System libraries remain accessible. Default: all reads allowed. |
--deny-read <paths> | --deny-read=/secret | Block reading from these paths. Takes precedence over --allow-read. |
--allow-write [paths] | --allow-write=. | Allow writing to these paths. Without a value, allows writing everywhere. Default: no writes. |
--deny-write <paths> | --deny-write=./.git | Block writing to these paths. Takes precedence over --allow-write. |
--allow-net [domains] | --allow-net=example.com | Allow outbound network. Without a value, allows all domains. Default: no network. |
--deny-net <domains> | --deny-net=evil.com | Block network to these domains. Takes precedence over --allow-net. |
--env <KEY=VALUE> | --env NODE_ENV=prod | Set env var in the sandbox. Can be repeated. |
--allow-env [keys] | --allow-env=PATH,HOME | Inherit parent env vars. Without a value, inherits all. Default: only PATH, HOME, USER, SHELL, TERM, LANG. |
--deny-env <keys> | --deny-env=SECRET | Drop these parent env vars. Takes precedence over --allow-env. |
--secret <KEY=VALUE> | --secret API_KEY=sk-123 | Pass a secret. The process sees a placeholder; the real value is injected at the proxy for approved hosts. |
--secret-host <KEY=HOSTS> | --secret-host API_KEY=api.openai.com | Restrict a secret to specific hosts. Without this, the secret is substituted for all hosts. |
-A, --allow-all | -A | Grant all filesystem and network permissions. Env and secrets still apply. |
--no-sandbox | --no-sandbox | Disable the sandbox entirely. |
--strict-sandbox | --strict-sandbox | Require full sandbox (bubblewrap). Fail instead of falling back to weaker isolation. |
--debug | --debug | Print sandbox config and proxy decisions to stderr. |
--snapshot | --snapshot | Record filesystem changes during execution. |
--restore | --restore | Record and restore tracked files to pre-execution state after exit. Implies --snapshot. |
--snapshot-path <paths> | --snapshot-path=./src | Paths to track for snapshots (default: cwd). |
--snapshot-exclude <patterns> | --snapshot-exclude=build | Exclude patterns from snapshots. |
-C <dir> | -C /workspace | Set working directory for the sandboxed command. |
-V, --version | --version | Print version. |
-h, --help | --help | Print help. |
Snapshot subcommands
| Command | Description |
|---|---|
zerobox snapshot list | List recorded sessions. |
zerobox snapshot diff <id> | Show changes from a session. |
zerobox snapshot restore <id> | Restore filesystem to a session's baseline. |
zerobox snapshot clean --older-than=<days> | Remove old snapshot sessions. |
License
Apache-2.0