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
$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 claudeClaude Code, contained$sandbox-cli run -- basha shell in the sandbox$sandbox-cli run --dry-run -- npm testprint 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.
~/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-runEvery 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-hardeningEvery 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
--runtimePoint 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-allowNothing 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
--secretResolve 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-authEach 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.
~/.config/sandbox/agents/claude -> /sandbox/home
- opt-in
Egress allowlist
--allowSwitch 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
--publishNo 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-gatewayAn 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
--worktreeRun 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
--gitForwards 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
--shareTwo 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
--pasteYour 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
--cacheContainers 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-metricsNon-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-statuslineClaude 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-runHighlighted 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 defaultsNon-root sandbox user, /workspace, a fake HOME, all capabilities dropped.
- 2you, everywhere
~/.config/sandbox/config.yamlPreferences that follow you across every project on this machine.
- 3the project
Nearest .sandbox.yamlFound by walking up from the current directory. Commit it; everyone on the repo gets the same boundary.
- 4this one run
Command-line flagsScalar 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_KEYnetwork: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
imagestringunset → 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.
workdirpathunset → /workspaceWhere the project is mounted and where the guest command starts.
usersandbox | 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.
homepathunset → /sandbox/homeThe fake HOME. Nothing under it is host-connected unless an agent wrapper persists its login there.
hostnamestringunset → sandboxThe container hostname, which is what an agent's prompt shows.
runtimestringunset → 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.
mountslist 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.
envmapunset → emptyLiteral values injected into the container. Merged key by key, so a project file can add one without wiping your user-level set.
env_allowlistunset → 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.
secretsmap 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.modedefault | 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.allowlistunset → 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.
portslistunset → 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_privilegesboolunset → trueBlocks setuid privilege escalation inside the container.
security.cap_drop / cap_addlistunset → [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_limitintunset → 1024A fork-bomb guard set well above real build and agent process counts. 0 disables it.
security.memory / cpusstringunset → unlimitedOpt-in resource caps, e.g. 2g and 1.5. Empty leaves the container unbounded — sandbox-cli measures usage rather than throttling it.
security.seccompstringunset → docker's default profilePoint at your own seccomp profile when the default one blocks something you need.
cache.enabled / cache.pathsbool / 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 / retentionbool / 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.comthe model the agent is running onBaselineregistry.npmjs.orgnpm install, still workingBaselinegithub.comgit fetch, git pushBaselinepypi.orgpip installBaselinefiles.pythonhosted.orgthe wheels themselvesBaselineraw.githubusercontent.cominstall scriptsBaselineinternal.registry.example.comyour private registry — added with --allow--allowapi.continue.devan agent's own config endpoint, added with --allow--allowpaste.example.netthe exfiltration a prompt-injected agent was talked intoDeniedwebhook.attacker.tldyour .env, POSTed somewhere elseDeniedcrypto-pool.examplea miner the dependency chain brought alongDeniedtelemetry.unknown-vendor.iophone-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/appYour 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-dk0gtrd15s2gmem 412MiBcpu 82% - feature-b
-p "port the tests"sandbox-9f2la8hq4vznmem 308MiBcpu 61% - docs/rewrite
-p "rewrite the guide"sandbox-m4x1pq7bd0csmem 196MiBcpu 24%
git diff main...feature-asandbox-cli worktree commit feature-a -m "…"sandbox-cli worktree rm feature-aYou 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/-workspacesandbox-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_KEYANTHROPIC_AUTH_TOKENANTHROPIC_BASE_URLCLAUDE_CODE_USE_BEDROCKCLAUDE_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).
- sandbox-cli
- Built-in agent sandboxes
- Cloud microVMs
Only the emphasised series is labelled; hover any bar for its score, and the table above is the full, readable version of the same judgement. Isolation is the axis where sandbox-cli deliberately does not win — a shared-kernel container is not a microVM, which is why --runtime exists.
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.
$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.