# zerobox: a repackaged sandbox runtime with five install channels, two of which pipe unreviewed code into a shell

> A Rust workspace that vendors another project's sandboxing crates under an upstream directory, layers credential injection at the network proxy and filesystem snapshots on top, and publishes the result through a shell installer, npm, PyPI and cargo. Versioning runs through a JavaScript changeset tool, and every internal dependency is pinned to one exact version.

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

- Repository: https://github.com/afshinm/zerobox
- Stars: 719 · Forks: 42
- Language: Rust
- License: Apache-2.0
- Published: 2026-09-10 · Updated: 2026-09-10 · Language: en
- Canonical page: https://hysenlabs.com/projects/afshinm-zerobox

## Two of the five install channels run code nobody has looked at

The install table lists five channels. Three are ordinary package managers: a global npm install, a PyPI install, and a cargo install. The other two are not. The shell channel for macOS and Linux pipes a fetched script directly into a shell, with the URL pointing at the raw file endpoint on the main branch, so what you run is whatever that branch contains at the moment you run it, with no version and no checksum. The from-source channel clones the repository, changes into it, runs a script called sync, and then builds one package in release mode. That sync step is the interesting one, because the workspace contains an upstream directory holding crates that did not originate in this project, including the sandboxing, linux sandbox, process hardening, network proxy and a Windows sandbox crate, and a file at the root recording an upstream version. Nothing in the visible text says what the sync script fetches, from where, or what it checks.

## Three releases in three hours, versioned by a JavaScript tool

The three most recent tags are all from the same day and within about three hours of each other: v0.3.1 in the early evening, v0.3.2 forty minutes later, and v0.3.3 another hour after that. The workspace version in the Cargo manifest is 0.3.3, matching the newest tag, and the last push to the repository carries the same timestamp as the newest release. So the day the project last moved, it shipped three times. How that number gets set is the more interesting mechanism. The root is a JavaScript workspace using pnpm, and its scripts are: build, test and typecheck delegating to a filter on the npm package, a changeset command, a version command that runs the changeset tool and then a version sync script, and a release command that builds and publishes. The development dependencies are the changeset CLI and its GitHub changelog plugin. A Rust workspace whose version is bumped by a JavaScript release tool, with a shell script copying the result into the Cargo manifests, is a two-language version pipeline for a project whose product is a single binary.

## Every internal crate is pinned to one exact version number

The workspace dependency table gives every internal crate a version requirement written as an exact equality rather than a range. The sandboxing crate, the Linux sandbox, the Windows sandbox, process hardening, the network proxy, the protocol crate, the snapshot crate, the telemetry crate, and three small utility crates for absolute paths, pseudo-terminals and strings all sit at the same number, each with a path pointing into either the vendored upstream tree or the project's own crates directory. That is a deliberate choice with two consequences. Upgrading anything means moving the whole set, which removes the class of bug where a new binary talks to an old library over a protocol it assumed was stable. It also means there is no supported way to pin one component back, and it explains why the version sync script has to touch many files at once. The public crates that carry the version are the ones users install, and they are all at the same number for the same reason.

## A secret with no host restriction is sent to every domain the process reaches

Credential injection works by substitution at the proxy, and the sequence is spelled out in the document:

```
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
```

The restriction that makes this safe is the host option, which binds a secret to a specific domain, and the documentation shows the pattern with two secrets bound to two different hosts on one command line. What it also documents is the case without that option: the secret is passed to all domains. So the failure mode of forgetting a flag is not a crash, it is a key handed to any host the sandboxed process contacts, including one named in data the process reads. Since network access is denied by default and granted per domain, the practical exposure depends on how much you allow, which is why the allow-network flag and the secret-host flag belong in the same review of a command line.

## Node's fetch ignores the proxy unless you pass the right flag

One note in the secrets section is the kind that saves an afternoon. Node's fetch does not respect the standard proxy environment variables by default, so a Node process inside the sandbox with secrets will not have its placeholder substituted unless you pass the argument that makes it use the environment proxy. The failure is quiet in the worst way: the request still goes out, carrying the placeholder instead of the key, so the call appears to work and simply is not authenticated, and no error tells you the proxy was bypassed. The note says the same thing applies to any Node code using secrets, which given that the tool's own examples are Node commands and that one of the bundled examples uses a JavaScript agent library, is a caveat that applies to a lot of realistic use. If your agent runtime is Node, treat that flag as mandatory rather than optional and test it before you rely on credential injection.

## Every benchmark row is exactly ten milliseconds, measured on one Apple machine

The performance table reports five commands with a bare time, a sandboxed time, the difference, and memory for each. The differences are identical in all five rows: ten milliseconds, whether the command is echoing a string, running a Node expression, running a Python expression, reading a ten megabyte file, or making a network request. That pattern says the overhead is a fixed setup cost paid once per sandbox rather than anything proportional to the work, which is consistent with a proxy process being started and torn down around each command. Memory tells the same story, with the sandbox adding roughly seven megabytes, matching the stated figure, and one row where the sandboxed run appears to use slightly less memory than the bare one, which is measurement noise rather than a saving. The conditions are stated: best of ten runs with warmup on an Apple M5 Pro, reproducible with a script in the bench directory. There is no Linux row, so the cost of this design on a server is not stated.

## Windows crates are vendored while Windows support is planned, and the platform table stops mid-word

The workspace includes a Windows sandbox crate in its member list, sitting alongside the Linux sandbox and the process hardening code around it. That is consistent with the sandbox being a repackaging of an upstream runtime that supports Windows, while this project's own feature list describes macOS and Linux as supported and Windows as planned. So the code for Windows is present in the tree and compiled into the workspace, but the user-facing support statement does not include it. The platform table that would have spelled out which backend is used on which platform is cut off after the macOS row, at a backend name beginning with Sea, so the one place the document would tell you what mechanism is doing the work on macOS is exactly the part that is not there. If you are on macOS and want to know whether you are on Seatbelt or something else, that table is the answer and the table is incomplete.

## Conclusion

Adopt this if you are running generated or untrusted code on a Mac or Linux box and want the same containment model an agentic coding tool uses, with credential injection and filesystem rollback added on top. The design is the reason to trust it: the process sees a placeholder rather than a key, substitution happens inside the proxy for named hosts only, and the default is deny for writes, network and environment variables, which is the correct direction for defaults. Three things to settle before you depend on it. The five install channels have very different risk profiles, and the shell route pipes a script from the moving main branch into a shell while the from-source route requires running a sync script whose purpose the documentation never explains. Every internal crate is pinned to one exact version, so the whole workspace moves in lockstep and you cannot mix a new binary with an old library. And the runtime caveats are real: Node's own fetch ignores the proxy environment unless you pass a specific flag, and a secret with no host restriction is sent to every domain the process contacts.

## FAQ

### What does the zerobox sandbox actually do?

It runs a command with writes, network access and environment variables blocked unless you allow them, lets you allow or deny reads, writes and domains individually, and injects credentials so the sandboxed process sees a placeholder rather than the real value. It also records filesystem changes so they can be inspected and undone.

### How does zerobox handle API keys?

The process reads a placeholder from the environment variable, and the real value is substituted at the network proxy level only for the hosts you name with --secret-host. Without a host restriction the secret is passed to all domains the process contacts.

### Can I undo filesystem changes made inside the sandbox?

Yes. The --restore flag records changes and undoes them after the run, and --snapshot records without restoring, after which a snapshot list, a diff against a session id and a restore of that session let you inspect and undo later.

### Which platforms and languages does zerobox support?

macOS and Linux, with Windows described as planned even though a Windows sandbox crate is vendored in the workspace. It ships as a single binary with no Docker and no virtual machines, and SDKs exist for Rust, TypeScript and Python behind a consistent API.

### What overhead does the zerobox sandbox add?

The stated figures are about ten milliseconds and roughly seven megabytes. The table reports identical ten-millisecond additions across five different commands, measured as best of ten runs with warmup on an Apple M5 Pro, reproducible with the benchmark script in the bench directory.

## Sources

- [afshinm/zerobox on GitHub](https://github.com/afshinm/zerobox)
- [Issues](https://github.com/afshinm/zerobox/issues)
- [License: Apache-2.0](https://github.com/afshinm/zerobox/blob/main/LICENSE)
- [README](https://github.com/afshinm/zerobox/blob/main/README.md)
- [Releases](https://github.com/afshinm/zerobox/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/afshinm-zerobox
