boxer

Security

What the sandbox isolates on each backend, what it does not, and the paths that stay on the host on purpose.

Summary

IsolatedCommands 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 purposegit, 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 guestThe worktree, read-write. Anything in mounts. Host package caches, read-only, if [cache] is on.
Exposed to the hostPorts in network.ports, bound to 127.0.0.1.
Threat modelA 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 (~/.npm for package-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.com and index.docker.io; for ghcr.io, mirror.gcr.io and public.ecr.aws, the registry and its blob host. A local image (./image.tar, a build) 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.com and 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_setup needs, such as deb.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.

BackendBoundaryEgress
smolvm (default)a kernel per sandboxallowlist by default
Apple containera kernel per sandboxon or off, no allowlist
docker, podmanone kernel shared by every sandbox on the machineon 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.

PathWhy it is on the host
passthrough programsgit, 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 nameIn 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 serversThese run on the host. See Repository configuration is executable input.
The skill on its ownA 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 itselfIt 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 claude

The 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.

MeasureResult
One Next.js worktree to HTTP 200smolvm and Apple container take 1 to 2 s longer than docker
Three worktrees to HTTP 200smolvm and Apple container take 3.5 to 4.7 s longer than docker
A warm command33 ms on smolvm against 29 ms for docker exec
A file-heavy command, such as a recursive grep2.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 guestRuns 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, which ps shows to every process on the host.
  • The container backends get the same by another route: boxer passes -e NPM_TOKEN with no value, and docker, podman or Apple's container read 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.

On this page