Security
What the sandbox isolates on each backend, what it does not, and the paths that stay on the host on purpose.
Summary
| Isolated | Commands on intercept run in the guest. They cannot read your home directory, SSH keys, cloud credentials or any other host path boxer does not mount. |
| On the host on purpose | git, gh, ssh, boxer and smolvm (passthrough). Hooks and MCP servers named in the harness's configuration. Commands not on intercept. [prep] commands. |
| Exposed to the guest | The worktree, read-write. Anything in mounts. Host package caches, read-only, if [cache] is on. |
| Exposed to the host | Ports in network.ports, bound to 127.0.0.1. |
| Threat model | A careless or destructive agent. boxer is not built to stop an agent that sets out to reach the host. |
boxer protects your machine from what a careless agent's builds, installs and scripts do: a
dependency's postinstall script, a build step that runs rm -rf in the wrong directory, a change to
system configuration. Those run in the sandbox because the program that starts them (npm, make,
python, ...) is on the intercept list. A postinstall script in the sandbox cannot read your home
directory, and a command that changes system configuration changes only the sandbox.
A program that is not on the list runs on your machine, and by default that includes rm, mv and
curl typed on their own. To send them to the sandbox too, add them with intercept_also, or set
intercept = ["*"], which sends everything but passthrough.
The worktree is your real worktree, mounted read-write. rm -rf inside it deletes your files from
the guest the same as from the host. The sandbox protects the rest of your machine. It does not
protect the code the agent is working on.
What the guest can reach
A sandbox sees only what boxer mounts into it:
- the git worktree, read-write
- the host's package caches for the lockfiles the worktree has (
~/.npmforpackage-lock.json, and so on), read-only, when[cache] enabled = true - anything you list in
mounts
On the default backend, smolvm, the network is an allowlist. It holds the registry hosts the image
needs plus the hosts in network.allow_hosts:
- Always added: the image's registry, so the guest can pull it. For Docker Hub that is
registry-1.docker.io,auth.docker.io,production.cloudflare.docker.comandindex.docker.io; forghcr.io,mirror.gcr.ioandpublic.ecr.aws, the registry and its blob host. A local image (./image.tar, abuild) adds none. - In inside mode, also: the npm registry and
deb.debian.org, to install the harness, the AI Gateway, and every harness's API and login hosts (api.anthropic.com,api.openai.comand so on), because the harness itself runs in the guest. There is no setting to leave them out. - Names and addresses: a hostname is matched exactly and resolved once, when the sandbox starts. A name that is not listed does not resolve in the guest, even if its address falls in an allowed CIDR block; reach such a host by address, or list the name.
- For the sandbox's whole life: there are no phases. A host
image_setupneeds, such asdeb.debian.org, stays reachable while the agent works.
What the allowlist does not cover
The allowlist is the sandbox's network, and nothing else. The agent's harness runs on your machine
(outside inside mode) with your machine's network: its web tools, its MCP servers, and every program
that runs on the host, git, gh and ssh included, reach whatever your machine can. boxer alone
does not keep an agent off the internet. For that, block the harness's web tools in its own
settings, and put a proxy or firewall in front of the machine.
Nothing stops an agent from editing boxer's own configuration either. A worktree's boxer.toml
overrides the repository's, and an agent can write one, or edit .claude/settings.json, with its
file tools. There is no layer an administrator controls that a repository cannot override. Review
changes to boxer.toml and the harness settings the way you review CI configuration (a CODEOWNERS
entry, for one).
Backends
The strength of the boundary depends on the backend. boxer doctor
prints which one you have.
| Backend | Boundary | Egress |
|---|---|---|
smolvm (default) | a kernel per sandbox | allowlist by default |
Apple container | a kernel per sandbox | on or off, no allowlist |
docker, podman | one kernel shared by every sandbox on the machine | on or off, no allowlist |
- A backend that cannot enforce the allowlist refuses
network.mode = "allowlist". It does not run with a network boxer said it would deny. - A container escape on a shared-kernel backend reaches every sandbox on the machine. On a kernel-per-sandbox backend it reaches one guest.
If containment is why you use boxer, keep the default backend.
What runs on the host
These run on the host. A hostile agent can use any of them to reach your machine.
| Path | Why it is on the host |
|---|---|
passthrough programs | git, gh, ssh, boxer and smolvm run unsandboxed by default. They need your credentials and your real repository. gh alone reaches anything your GitHub token reaches. |
Commands intercept does not name | In rewrite mode boxer rewrites the commands it recognises. Anything else runs on the host. PATH shims and tool mode catch some of these commands, but not all. enforcement = "both", the default, uses the shims as well as the hook, but boxer install writes none: run boxer shim install and put its directory on the harness's PATH. See integration levels. |
[prep] commands, hooks and MCP servers | These run on the host. See Repository configuration is executable input. |
| The skill on its own | A skill or plugin installed without boxer install only asks the agent to use boxer. See skills and plugins. |
mode = "off" and enforcement = "audit" | Neither sandboxes anything. audit prints what boxer would have rewritten or blocked, and runs the command on the host. |
on_sandbox_unavailable = "passthrough" | Off by default ("fail"). When set, a command whose sandbox cannot be provisioned runs on the host instead of failing. |
boxer itself | It is passthrough, so an agent can run boxer rm, boxer down --scope or boxer run --scope against any sandbox on the host, other worktrees' included. Filtered rm asks a person and refuses an agent without -y; nothing else limits it. |
Two facts about how a rewrite reaches the harness. Claude Code gets a rewritten command back
already approved (permissionDecision: "allow"), so it does not prompt for an intercepted line it
would have prompted for. And the harness gives the hook a time limit: in the Claude Code plugin,
30 seconds for each command and 600 for session start, which is when boxer provisions.
If the threat model is adversarial
Run the harness inside the guest:
integration = "inside"boxer shell claudeThe agent has no shell on the host. It can still change files the host later runs: the worktree is
mounted read-write, including a main checkout's .git/hooks, and so is every harness config
directory below. The worktree is read-write in every mode, so this holds outside inside mode too:
a command in the guest can write .git/hooks, .claude/settings.json and boxer.toml, and read
.git/config, where actions/checkout keeps its token unless told not to. A hook written into ~/.claude/settings.json or .git/hooks runs on the host the
next time you use that harness, or git, outside the sandbox.
Every harness config directory that exists on the host (~/.claude,
~/.codex, and so on) is mounted read-write into the guest. The harness's credentials are inside
the sandbox. See Run the harness inside.
Start-up and command cost
A kernel per sandbox costs start-up time.
| Measure | Result |
|---|---|
| One Next.js worktree to HTTP 200 | smolvm and Apple container take 1 to 2 s longer than docker |
| Three worktrees to HTTP 200 | smolvm and Apple container take 3.5 to 4.7 s longer than docker |
| A warm command | 33 ms on smolvm against 29 ms for docker exec |
| A file-heavy command, such as a recursive grep | 2.4x a warm container, because of the mount |
The numbers and method are in Benchmarks.
Repository configuration is executable input
A boxer.toml, a skill or a plugin in a repository is code you are about to run.
The worktree is mounted read-write, so code in the sandbox — the agent, or an npm postinstall it
triggers — can rewrite boxer.toml or devcontainer.json, which boxer reads on the host. The keys
that reach the host (prep, mounts, build, mode, enforcement, passthrough, intercept,
secrets, env_passthrough, network, on_sandbox_unavailable, telemetry.path and
[harness.*]) are held back until a person approves the configuration with
boxer trust. Until then the sandbox runs with boxer's defaults
for those keys, so a configuration the guest planted cannot run a host command or widen the sandbox
on its own. A change to any of those keys has to be approved again. boxer's own git is also run
with core.fsmonitor, hooks, the pager and external diff turned off, so a .git/config the guest
writes cannot run a command when boxer reads the worktree's git state.
This does not cover everything. A repository you clone and trust can still ship a hostile
configuration, and boxer trust only confirms that a person saw it, not that it is safe. Review a
boxer.toml the way you review a CI configuration.
| Runs in the guest | Runs on the host |
|---|---|
setup, image_setup, start, [tasks] | [prep] commands, and hooks and MCP servers named in a harness's configuration |
Treat a repository's boxer configuration like its Makefile or its package.json install
scripts. boxer never fetches configuration or content from the network.
Forwarded ports and URLs
A port in network.ports lets the host reach the guest. boxer binds it to 127.0.0.1 on every
backend. Processes on this machine can reach it, and the local network cannot.
docker, podman and Apple container publish on every interface by default, so boxer passes the
loopback address to them explicitly. On those three, a spec that names an address passes through
unchanged. "0.0.0.0:3000:3000" exposes the port on purpose.
The portless proxy behind [urls] also listens on 127.0.0.1. boxer starts
it on port 1355 when a sandbox needs a name, and stops it once no route is left. A proxy you
started yourself keeps the options you gave it, and boxer never stops it.
Anything on this machine can reach a forwarded port. That includes other users' processes and a
browser tab sent to a localhost URL. Do not treat a dev server in the sandbox as private to the
agent.
Secrets
env_passthrough = ["CI"] # read from the host at run time
secrets = ["NPM_TOKEN"] # the same; never in a command line, a pack or a log
[env]
NODE_ENV = "development" # configuration. Baked into the pack.env is saved into the environment pack, which persists on disk. Put anything you would not want
in a snapshot in the other two lists. boxer resolves them on the host at run time and passes them
to the guest process. It stores them in no VM metadata and no file it keeps.
env_passthrough is an explicit allowlist, ["CI"] by default. In inside mode the harness's own
login variables (its API key or token) reach the guest too, by name like these two lists; no other
host variable does.
Both lists are forwarded by name:
- On smolvm, boxer passes
--secret-env NPM_TOKEN=NPM_TOKEN, and smolvm reads the value from its own environment. The value never appears in an argument vector, whichpsshows to every process on the host. - The container backends get the same by another route: boxer passes
-e NPM_TOKENwith no value, and docker, podman or Apple'scontainerread the value from their own environment, where boxer put it. A value of several lines, such as a PEM key, arrives whole. A secret whose name the CLI reads as one of its own settings (DOCKER_HOST) goes through a file only your user can read instead, deleted as soon as the command returns.
Both lists reach setup and image_setup as well as ordinary commands, so an install that needs a
registry token has it. They never reach the pack.
What boxer records
With no [telemetry] table and no BOXER_TRACE, boxer writes no log, no metrics and nothing to
the network. When enabled, events carry scope names, harness names,
durations and outcomes. Command lines are left out unless you also set record_commands = true.
boxer always writes one local file. After every command it writes
$XDG_STATE_HOME/boxer/runs/<scope>.json, mode 0600. The file holds that command verbatim, its exit
code, and a bounded tail of its output. boxer capsule new captures it and boxer status --json
reports it.
record_commands does not control this file. That setting guards the event sink (a file or an
OTLP endpoint), and either sink can be shipped off the host. You can delete the runs directory at
any time. boxer reads it only to report.
Nothing leaves the machine unless you set an endpoint. Only a binary built with -tags otel can
use one. The default build contains no exporter.
Report a vulnerability
Report it privately through GitHub's private vulnerability reporting. Expect an acknowledgement within three working days and a fix or a plan within fourteen. The latest minor release of the current major version is supported.