# Sandbox-CLI is now in public beta

> Source: <https://sandbox-cli.vercel.app>
> Published: 2026-07-25 20:58:39+00:00

# Give the agent full autonomy.

Give it nothing else.

sandbox-cli runs Claude Code, Codex, Gemini and twelve more coding agents inside a disposable Docker container. Only the project you point it at is mounted; HOME is a fake ephemeral path and your SSH keys, cloud credentials and browser cookies are not there to be read.

Needs Docker. Nothing else. macOS · Linux · WindowsMIT licensed · written in Go

[all releases](https://github.com/Amitgb14/sandbox-cli/releases)

```
$curl -fsSL https://raw.githubusercontent.com/Amitgb14/sandbox-cli/main/install.sh \   | sh -s -- --version 0.0.1beta.5
```

Detects your OS and CPU, verifies the archive against the release checksums.txt, and installs to ~/.local/bin/sandbox-cli. No root, no package manager.

then

`$sandbox-cli claude`

Claude Code, contained`$sandbox-cli run -- bash`

a shell in the sandbox`$sandbox-cli run --dry-run -- npm test`

print the docker argv, run nothing

- awaiting a command…

- 1
- host path mounted
- your project, at /workspace
- 15
- agents wrapped
- one prefix, flags forwarded verbatim
- 0
- host creds forwarded
- default-deny env allowlist
- --rm
- every container
- nothing survives the run

## “Allow All” is the mode that makes agents useful.

It is also the one that scares you.

Agents earn their keep the moment they stop asking permission for every edit. But the same flag that unblocks the work hands a non-deterministic process your entire home directory — and prompt injection turns text somebody else wrote into commands your shell runs.

- Reads ~/.ssh, ~/.aws, cloud tokens, browser cookies
- One hallucinated path and the blast radius is your whole disk
- A poisoned README turns into local execution
- The only thing enforcing trust is the prompt

- Those paths were never mounted — there is nothing to read
- The blast radius is /workspace: the repo it was editing anyway
- Injection lands in a container that dies on exit
- The kernel enforces trust, not a paragraph of instructions

This is the default when you run an agent with “Allow All” on your machine. Pick a path to read what is at stake.

### One function decides everything

runtime.BuildArgs is pure and deterministic: config in, docker argv out. It is the single choke point for what the container can reach, and it is exhaustively unit-tested against a golden output so the boundary cannot drift silently.

### Three refusals no flag can override

Never mount /, never mount your home directory, never mount an ancestor of it. ResolveWorkspace enforces those before anything else runs, and there is no configuration key that turns them off.

### Nothing crosses that you did not name

Host environment variables are default-deny. Each agent wrapper ships a small suggested allowlist applied only if the value is set, and everything else needs an explicit --env or --mount.

## Twenty-three capabilities, one prefix

Everything below ships today and is reachable from a flag or a `.sandbox.yaml`

key. Filter by the question you came with.

- on by default
### One host path, mounted on purpose

The project you chose is bind-mounted at /workspace and nothing else is host-connected. HOME, /etc and / inside the container are ephemeral and destroyed on exit.

``` php
~/projects/app  ->  /workspace     (the only host-connected path)
```

- on by default
### Refusals you cannot configure away

sandbox-cli refuses to mount /, your home directory, or any ancestor of it as the workspace. That check lives in ResolveWorkspace and no flag, config file or env var overrides it.

- on by default
### One pure function builds the argv

`--dry-run`

Every isolation decision funnels through runtime.BuildArgs — deterministic, no I/O, exhaustively unit-tested against a golden output. --dry-run prints the exact docker command and exits, so you can read the boundary before you trust it.

```
sandbox-cli run --dry-run -- npm test
```

- on by default
### Hardened container by default

`--no-hardening`

Every run drops all Linux capabilities, forbids privilege escalation, and caps the process count to blunt fork bombs. Non-root by default, which is also why agents accept --dangerously-skip-permissions in here.

```
--cap-drop ALL  --security-opt no-new-privileges  --pids-limit 1024
```

- opt-in
### Stronger isolation on request

`--runtime`

Point a run at any OCI runtime the daemon knows: kata-runtime for a microVM with its own kernel, runsc for gVisor's userspace syscall filter. Mounts, hardening, allowlist, caches and secrets all work unchanged on top.

```
sandbox-cli claude --runtime kata-runtime
```

- on by default
### Default-deny environment

`--env-allow`

Nothing from your host environment crosses the boundary unless you name it. Each agent wrapper ships a small suggested allowlist — ANTHROPIC_API_KEY and friends — applied only if the value is actually set.

- opt-in
### Credential broker

`--secret`

Resolve a secret at run time from a file, a host command, or a host env var, and forward it by name. The raw value never appears on the docker argv, in --dry-run output, in config, or in your shell history — and cmd: sources can be short-lived tokens fetched fresh each run.

```
--secret GITHUB_TOKEN=cmd:'gh auth token'
```

- on, --flag to disable
### Logins that survive --rm

`--no-persist-auth`

Each agent gets its own sandbox-owned directory bind-mounted as the container's whole HOME, so you authenticate once. It is separate from your real ~/.claude — the sandbox never reads or writes your host agent config.

``` php
~/.config/sandbox/agents/claude  ->  /sandbox/home
```

- opt-in
### Egress allowlist

`--allow`

Switch outbound traffic to default-deny with an in-container firewall. DNS, established flows and a baseline of agent APIs and package registries stay open, so npm install and git keep working while arbitrary exfiltration from a prompt-injected agent does not.

```
sandbox-cli claude --allow internal.registry.example.com
```

- on by default
### Fails closed

The firewall is programmed at startup with NET_ADMIN — added only in this mode — and the run then drops back to the non-root sandbox user. If setup errors, the run fails rather than silently continuing wide open.

- opt-in
### Publish a port when you want to look

`--publish`

No container port is reachable from the host until you ask. When you do, a spec that names no address binds to 127.0.0.1 rather than every interface — the one place sandbox-cli deliberately differs from docker -p. Put your dev-server ports in .sandbox.yaml and stop typing them.

```
sandbox-cli run -P 3000 -- npm run dev
```

- opt-in
### Reach host services deliberately

`--host-gateway`

An agent can talk to an MCP server on your machine through host.docker.internal. That resolves automatically on Docker Desktop; on Linux this flag maps it, and --add-host handles anything else.

- opt-in
### Parallel agents on real git worktrees

`--worktree`

Run several agents at once, each on its own branch, in its own container, with no collisions. The worktree lives in a sandbox-owned directory so your checkout stays clean, and the branch shows up in your repo immediately.

```
sandbox-cli claude --worktree feature-a -- -p 'implement A'
```

- opt-in
### Addressable by branch, never by cd

worktree list, path, git, commit and rm all take the branch name. worktree git forwards everything after it straight to git — output and exit code included — so your config, hooks and commit signing still apply.

```
sandbox-cli worktree commit feature-a -m 'implement A'
```

- opt-in
### Commits attributed to you

`--git`

Forwards your host user.name and user.email and marks the workspace trusted, so git in the container stops complaining about dubious ownership and the agent's commits carry your name.

- opt-in
### A channel between sandboxes

`--share`

Two sandboxes cannot see each other — that is the point, but it leaves no way to hand over an API contract. --share mounts one host directory at /shared in every sandbox that asks for it. Then just say so in the prompt.

- opt-in
### Pasted image paths that resolve

`--paste`

Your terminal pastes an image as an absolute host path, which names nothing inside a container. This mounts ~/Desktop, ~/Downloads and ~/Pictures read-only at their own host paths so the path resolves. Opt-in, because it widens what the agent can read.

- opt-in
### Package caches that persist

`--cache`

Containers are --rm, so a cold npm install every run gets old fast. --cache keeps npm, pip, cargo and go caches in named Docker volumes across runs — no host directory involved.

- on by default
### Layered project config

Built-in defaults, then ~/.config/sandbox/config.yaml, then the nearest .sandbox.yaml walking up from cwd, then flags. sandbox-cli config show prints what actually won.

```
sandbox-cli init      # scaffold .sandbox.yaml
```

- on, --flag to disable
### Live resource gauge

`--no-metrics`

Non-interactive runs pin a memory/CPU/elapsed gauge to the bottom of the terminal with the workspace's git branch at the right — the thing that tells parallel worktree sandboxes apart at a glance. Measurement only; no limits are imposed.

```
sandbox-cli │ mem 512MiB/7.6GiB [▓░░░░░░] cpu 82% · 0m47s   git:feature/login
```

- on, --flag to disable
### A status line inside Claude

`--no-statusline`

Claude Code renders the container's live memory and CPU in its own UI, injected through a managed-settings file that never touches your own Claude settings. Deliberately limited to claude: no other agent has a status-line hook, and running them under tmux to fake one made their TUIs render badly.

```
[sandbox] · mem 412MiB · cpu 82%                    git:feature/login
```

- on by default
### Peak summary on every run

Interactive sessions own the screen, so instead of drawing over them sandbox-cli samples in the background and prints one line when the run exits. You still get the numbers for a twelve-minute Claude session.

```
sandbox-cli: peak mem 412MiB · cpu peak 138% · 12m04s · git:feature/login
```

- opt-in
### Watch every sandbox at once

sandbox-cli stats is a refreshing table of all running sandbox containers in a second terminal — the answer for the agents that have no status line of their own. --once for a scriptable snapshot.

```
sandbox-cli stats --interval 1s
```

## Build the command. Read the argv. Then decide.

Every flag resolves into a plain `docker`

invocation, and `--dry-run`

prints it before anything executes. Toggle real options and watch the boundary widen or tighten — the arrow beside each flag tells you which way it moves.

agent

`sandbox-cli claude --allow --dry-run`

Highlighted lines are the ones your toggles added. Hover any line for what it does. The only host paths in the whole command are the `--mount`

sources — count them, and that is the blast radius.

## Everything above has a `.sandbox.yaml`

key

Flags are for one run; a `.sandbox.yaml`

committed at the root of the repo is the boundary everyone on the project gets by default. Nothing here is required — `sandbox-cli init`

scaffolds the file, and every key falls back to a working default.

precedence — later wins, key by key

- 1config.Default()
`Built-in defaults`

Non-root sandbox user, /workspace, a fake HOME, all capabilities dropped.

- 2you, everywhere
`~/.config/sandbox/config.yaml`

Preferences that follow you across every project on this machine.

- 3the project
`Nearest .sandbox.yaml`

Found by walking up from the current directory. Commit it; everyone on the repo gets the same boundary.

- 4this one run
`Command-line flags`

Scalar flags override the file for one run; the list-shaped ones — --mount, --env-allow, --allow, --publish — add to what it declared rather than replacing it.

`sandbox-cli config show`

prints the merged result for the directory you are standing in, and `sandbox-cli config path`

says which files it read.

`.sandbox.yaml`

```
# .sandbox.yaml — commit this with the project# Only /workspace is mounted; HOME is fake and dies with the container. env_allow:              # host vars forwarded ONLY if they are set  - ANTHROPIC_API_KEY  - OPENAI_API_KEY network:  mode: default         # default | none | allowlist
```

sandbox-cli init writes a fuller, fully-commented version of this file into the current directory. Zero config is required — every key below has a working default.

### Every key, and what you get without it

`image`

stringunset → built-in sandbox-baseThe base image tag is content-addressed (sandbox-base:<gen>-<hash>) so it rebuilds itself whenever the image definition changes. Pinning your own tag opts out of that.

`workdir`

pathunset → /workspaceWhere the project is mounted and where the guest command starts.

`user`

sandbox | rootunset → sandboxNon-root by default — which is also why agents accept --dangerously-skip-permissions in here; they refuse it as root. On macOS, bind-mount ownership is virtualized, so files are still written as you.

`home`

pathunset → /sandbox/homeThe fake HOME. Nothing under it is host-connected unless an agent wrapper persists its login there.

`hostname`

stringunset → sandboxThe container hostname, which is what an agent's prompt shows.

`runtime`

stringunset → docker default (runc)Any OCI runtime the daemon has registered: kata-runtime for a microVM with its own kernel, runsc for gVisor. Mounts, hardening, allowlist and caches all work unchanged on top.

`mounts`

list of { host, container, mode }unset → just /workspacehost may start with ~ and may be relative to the config file that declared it. mode is ro unless you say rw. Refusals no key overrides: never /, never your home directory, never an ancestor of it.

`env`

mapunset → emptyLiteral values injected into the container. Merged key by key, so a project file can add one without wiping your user-level set.

`env_allow`

listunset → the agent's own suggestionA default-deny allowlist of host variables, forwarded only when actually set. The one list that appends across layers instead of replacing.

`secrets`

map of { file | command | env }unset → noneBrokered credentials, resolved at run time and forwarded by name. Exactly one source per secret; a command: source can fetch a short-lived token fresh each run.

`network.mode`

default | none | allowlistunset → defaultallowlist programs a default-deny egress firewall inside the container at startup, then drops back to the non-root user. If it cannot be programmed, the run fails instead of running open.

`network.allow`

listunset → baseline onlyExtra domains on top of the built-in baseline — agent APIs plus the common package registries — so npm install and git keep working. Replaces rather than appends, so a project can fully redefine it.

`ports`

listunset → nothing publishedA spec with no address of its own binds to 127.0.0.1, not every interface — the one place sandbox-cli deliberately differs from docker -p. Write 0.0.0.0:3000:3000 to expose it on purpose.

`security.no_new_privileges`

boolunset → trueBlocks setuid privilege escalation inside the container.

`security.cap_drop / cap_add`

listunset → [ALL] / noneAll Linux capabilities are dropped, which is essentially free for the non-root sandbox user. Add one back only when a tool genuinely needs it.

`security.pids_limit`

intunset → 1024A fork-bomb guard set well above real build and agent process counts. 0 disables it.

`security.memory / cpus`

stringunset → unlimitedOpt-in resource caps, e.g. 2g and 1.5. Empty leaves the container unbounded — sandbox-cli measures usage rather than throttling it.

`security.seccomp`

stringunset → docker's default profilePoint at your own seccomp profile when the default one blocks something you need.

`cache.enabled / cache.paths`

bool / listunset → false / built-in dirsKeeps npm, pip, cargo and go caches in named docker volumes so a cold install every run stops hurting. No host directory is involved. Extra paths are added to the built-in set.

`snapshot.enabled / interval / retention`

bool / duration / durationunset → true / 2m / 336hThe crash safety net: the workspace is committed under refs/sandbox/snapshots/ while a run is in flight, never touching your index, HEAD, branches or working tree. sandbox-cli recover reads it back.

### Some lists append, the rest replace

mounts and env_allow accumulate across layers — a project adds a variable without restating yours — and env and secrets overlay key by key. network.allow, ports, cache.paths and the security lists replace instead, so a project can fully redefine a policy, and say “none” with an empty list.

### Relative paths follow their file

A mount written as ./data resolves against the directory of the config file that declared it, not your current directory — so the same .sandbox.yaml means the same thing no matter where in the repo you run from.

### Omitted is not false

enabled, no_new_privileges and pids_limit are tri-state: leaving a key out keeps the inherited value, while writing it out explicitly overrides it — which is how a project turns a default-on setting off.

## A firewall that stops exfiltration without stopping npm install

Filesystem isolation does nothing about an agent that has been talked into POSTing your .env somewhere. Flip the allowlist and watch which requests still leave.

`api.anthropic.com`

the model the agent is running onBaseline`registry.npmjs.org`

npm install, still workingBaseline`github.com`

git fetch, git pushBaseline`pypi.org`

pip installBaseline`files.pythonhosted.org`

the wheels themselvesBaseline`raw.githubusercontent.com`

install scriptsBaseline`internal.registry.example.com`

your private registry — added with --allow--allow`api.continue.dev`

an agent's own config endpoint, added with --allow--allow`paste.example.net`

the exfiltration a prompt-injected agent was talked intoDenied`webhook.attacker.tld`

your .env, POSTed somewhere elseDenied`crypto-pool.example`

a miner the dependency chain brought alongDenied`telemetry.unknown-vendor.io`

phone-home nobody asked forDenied

`iptables`

inside the container at startup and then dropped back to the non-root user. It fails closed. Domains resolve to IPs once at startup, so hosts behind rotating CDN addresses can still be refused — add them explicitly.## Three agents, three branches, one repo, zero collisions

Isolation stops being a tax the moment it lets you do something you could not do before. `--worktree`

runs each agent in a real git worktree for its own branch, in its own container, so you can start three and go and do something else.

`~/projects/app`

Your checkout, untouched and still on whatever branch you had. The worktrees live in a sandbox-owned directory, so the project folder stays clean.

`~/.config/sandbox/worktrees/app-9f95/<branch>`

- feature-a
`-p "implement the API"`

`sandbox-dk0gtrd15s2g`

mem 412MiBcpu 82% - feature-b
`-p "port the tests"`

`sandbox-9f2la8hq4vzn`

mem 308MiBcpu 61% - docs/rewrite
`-p "rewrite the guide"`

`sandbox-m4x1pq7bd0cs`

mem 196MiBcpu 24%

`git diff main...feature-a`

`sandbox-cli worktree commit feature-a -m "…"`

`sandbox-cli worktree rm feature-a`

## You can see what it is doing

A sandbox you cannot watch is a sandbox you will not trust. sandbox-cli measures — it never throttles — and reports in three places.

### Why only Claude gets a status line

Claude Code has a `statusLine`

hook, so the gauge lives in its own UI, injected through a managed-settings file that never touches your Claude settings. Neither Gemini CLI nor OpenCode has such a hook. Running them inside tmux to fake one was tried and reverted — it made their TUIs render badly, which is a bad trade for a gauge.

For every other agent the answer is `sandbox-cli stats`

in a second terminal, plus the peak-usage line every run prints when it exits.

shared history

`~/.claude/projects/<project> -> /sandbox/home/.claude/projects/-workspace`

sandbox-cli claude mounts your host Claude history for the current project so --resume inside the sandbox lists the sessions you started on the host, and sessions you run inside show up outside afterwards. Only that one project's directory is mounted, it is read-write, and --no-sync opts out.

non-interactive run — the sticky footer gauge

inside a sandbox-cli claude session — Claude's own status line

after every run, interactive included — the peak summary

## 15 agents, one prefix, your flags forwarded verbatim

`sandbox-cli claude --dangerously-skip-permissions`

just works: a leading run of sandbox flags is consumed by sandbox, and the first token that is not one ends it — everything after goes to the agent untouched. 4 agents are baked into the base image; the rest install themselves into their own persisted home on first use, so you only download what you actually run.

### Claude Code

Anthropic

`sandbox-cli claude --dangerously-skip-permissions`

- login
- Run it and follow the prompt — a Claude account or ANTHROPIC_API_KEY.
- persisted at
`~/.config/sandbox/agents/claude -> /sandbox/home`

forwarded only if set

`ANTHROPIC_API_KEY`

`ANTHROPIC_AUTH_TOKEN`

`ANTHROPIC_BASE_URL`

`CLAUDE_CODE_USE_BEDROCK`

`CLAUDE_CODE_USE_VERTEX`

The only wrapper with a live memory/CPU status line in the agent's own UI, and the only one that shares your host conversation history for this project so --resume works on both sides.

## Where this sits, including where it loses

Running an agent in a disposable container is a crowded space. sandbox-cli's edge is code quality, ergonomics and a focused feature set — not a hard security boundary. If you need one of those, the table says so.

| sandbox-clithis project | Built-in agent sandboxesClaude / Codex | Docker Sandboxessbx | Native OS toolsSeatbelt / Landlock | Cloud microVMsE2B, Daytona, … | |
|---|---|---|---|---|---|
| Isolation strengthHow hard the wall actually is | Good — Docker + hardening, optional gVisor/Kata | Medium — OS-level, shared kernel | Excellent — microVM / Firecracker | Good — kernel primitives | Excellent — microVMs |
| Local, no cloudYour code never leaves the machine | Yes | Yes | Yes | Yes | No |
| Persistent agent authLog in once, not every run | Excellent — dedicated persistent home | Varies | Good | Varies | Varies |
| Package cache persistenceNo cold npm install every run | Yes — --cache volumes | Limited | Good | Manual | Often built-in |
| Parallel agents (worktrees)Several branches at once, no collisions | Excellent — built-in --worktree | Poor | Good | Poor | Varies |
| Credential brokerSecrets off the argv and out of history | Excellent — file / cmd / env sources | Basic | Good — proxy | Varies | Good |
| Egress / network controlStop exfiltration, keep installs working | Strong — allowlist with baselines | Basic | Strong | Varies | Strong |
| Observability / metricsWhat is this thing actually doing | Excellent — live gauge, stats, summaries | Limited | Good | Poor | Varies |
| Project configPer-repo policy, checked in | Excellent — .sandbox.yaml | Limited | Good | Poor | API / config |
| Dry-run / previewRead the boundary before trusting it | Yes | No | Varies | No | Varies |
| Ease of use | High — CLI-focused, thorough docs | High | High | Medium | Medium — setup |
| Cross-platform | Good — macOS / Linux / Windows | Good | Excellent | Platform-specific | N/A |
| Docker dependency | Yes | No | Yes | No | No |
| Best for | Local multi-agent workflows, ergonomics | Quick minimal protection | Strongest local isolation | Lightweight, zero deps | Scale & long-running tasks |

This is the project’s own read of the landscape, and the ratings for other tools are a snapshot that will age — check their docs before choosing. sandbox-cli’s edge is code quality and a focused feature set, not a hard security boundary; for that, reach for microVM tooling (or run sandbox-cli on top of one with `--runtime`

).

## Everywhere Docker runs

Almost everything works identically across platforms; the differences are all about the boundary the host can provide.

| Capability | macOSDocker Desktop | Linuxnative Docker | WindowsDocker Desktop / WSL2 |
|---|---|---|---|
| run, agent wrappers, mounts, env, hardening, metrics | supported | supported | supported |
| --cache, --secret, --worktree, --git, --share | supported | supported | supported |
| Egress allowlist (--allow)The firewall runs iptables inside the Linux container, so it works wherever the container kernel is Linux. Verified in CI on native Linux; not yet independently verified on Docker Desktop. | partly verified | supported | partly verified |
| --host-gatewayhost.docker.internal resolves automatically on Docker Desktop; native Linux needs the flag. | auto | needed | auto |
| /workspace file ownershipOn native Linux, use --user "$(id -u):$(id -g)" if ownership matters. | virtualized to you | container uid | virtualized to you |
| --runtime kata-runtime / runscDocker Desktop runs containers in its own managed Linux VM and won't let you register custom OCI runtimes — you already get a VM boundary from Docker Desktop itself. | not available | supported | not available |

## Install once. Prefix your agent. Done.

Needs Docker — Docker Desktop on macOS and Windows. Go 1.25+ only if you build from source. The first run builds the base image; every run after that starts immediately.

Uninstalling is symmetrical and cautious: `--uninstall`

removes the binary and then *reports* what else is on disk without deleting it — because `~/.config/sandbox`

holds your agent logins, and silently deleting it would sign you out of everything with no warning. Add `--purge`

when you mean it.

[all releases](https://github.com/Amitgb14/sandbox-cli/releases)

```
$curl -fsSL https://raw.githubusercontent.com/Amitgb14/sandbox-cli/main/install.sh \   | sh -s -- --version 0.0.1beta.5
```

Detects your OS and CPU, verifies the archive against the release checksums.txt, and installs to ~/.local/bin/sandbox-cli. No root, no package manager.

v0.0.1beta.5 · verified against the release `checksums.txt`

· installs to `~/.local/bin`

· no root, no package manager.
