ZEROBOX(1)

NAME

zeroboxLightweight, cross-platform process sandboxing powered by OpenAI Codex's runtime. Sandbox any command with file,…

SYNOPSIS

$npm install -g zerobox

INFO

556 stars
30 forks
0 views

DESCRIPTION

Lightweight, cross-platform process sandboxing powered by OpenAI Codex's runtime. Sandbox any command with file, network, and credential controls.

README

🫙 Zerobox

Sandbox any command with file, network, and credential controls.

Zerobox npm version Zerobox license Zerobox CI status

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::Sandbox with 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

Zerobox Sandbox Flow

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

Zerobox 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 fetch does not respect HTTPS_PROXY by default. When running Node.js inside a sandbox with secrets, make sure to pass the --use-env-proxy argument.

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(&quot;fs&quot;).readFileSync(&quot;/tmp/input.txt&quot;, &quot;utf8&quot;); console.log(JSON.stringify({ content }));.json();

await writer.js require(&quot;fs&quot;).writeFileSync(&quot;/tmp/output.txt&quot;, &quot;result&quot;); console.log(&quot;ok&quot;);.text();

const result = await fetcher.js const res = await fetch(&quot;https://example.com&quot;); console.log(JSON.stringify({ status: res.status }));.json();

Full working examples:

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:

CommandBareSandboxedOverheadBare MemSandbox Mem
echo hello<1ms10ms+10ms1.2 MB8.4 MB
node -e '...'10ms20ms+10ms39.3 MB39.1 MB
python3 -c '...'10ms20ms+10ms12.9 MB13.0 MB
cat 10MB file<1ms10ms+10ms1.9 MB8.4 MB
curl https://...50ms60ms+10ms7.2 MB8.4 MB

Best of 10 runs with warmup on Apple M5 Pro. Run ./bench/run.sh to reproduce.

Platform support

PlatformBackendStatus
macOSSeatbelt (sandbox-exec)Fully supported
LinuxBubblewrap + Seccomp + NamespacesFully supported
WindowsRestricted Tokens + ACLs + FirewallPlanned

CLI reference

FlagExampleDescription
--allow-read <paths>--allow-read=/tmp,/dataRestrict readable user data to listed paths. System libraries remain accessible. Default: all reads allowed.
--deny-read <paths>--deny-read=/secretBlock 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=./.gitBlock writing to these paths. Takes precedence over --allow-write.
--allow-net [domains]--allow-net=example.comAllow outbound network. Without a value, allows all domains. Default: no network.
--deny-net <domains>--deny-net=evil.comBlock network to these domains. Takes precedence over --allow-net.
--env <KEY=VALUE>--env NODE_ENV=prodSet env var in the sandbox. Can be repeated.
--allow-env [keys]--allow-env=PATH,HOMEInherit parent env vars. Without a value, inherits all. Default: only PATH, HOME, USER, SHELL, TERM, LANG.
--deny-env <keys>--deny-env=SECRETDrop these parent env vars. Takes precedence over --allow-env.
--secret <KEY=VALUE>--secret API_KEY=sk-123Pass 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.comRestrict a secret to specific hosts. Without this, the secret is substituted for all hosts.
-A, --allow-all-AGrant all filesystem and network permissions. Env and secrets still apply.
--no-sandbox--no-sandboxDisable the sandbox entirely.
--strict-sandbox--strict-sandboxRequire full sandbox (bubblewrap). Fail instead of falling back to weaker isolation.
--debug--debugPrint sandbox config and proxy decisions to stderr.
--snapshot--snapshotRecord filesystem changes during execution.
--restore--restoreRecord and restore tracked files to pre-execution state after exit. Implies --snapshot.
--snapshot-path <paths>--snapshot-path=./srcPaths to track for snapshots (default: cwd).
--snapshot-exclude <patterns>--snapshot-exclude=buildExclude patterns from snapshots.
-C <dir>-C /workspaceSet working directory for the sandboxed command.
-V, --version--versionPrint version.
-h, --help--helpPrint help.

Snapshot subcommands

CommandDescription
zerobox snapshot listList 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

SEE ALSO

clihub4/15/2026ZEROBOX(1)