v0.0.1beta.5 · out now

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

v0.0.1beta.5latest release
all 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 claudeClaude Code, contained
  • $sandbox-cli run -- basha shell in the sandbox
  • $sandbox-cli run --dry-run -- npm testprint the docker argv, run nothing
your machine/workspace
  • 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
the trade nobody should have to make

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

Agent on the bare host
  • 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
Agent inside sandbox-cli
  • 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
12of 12 host locations in reach

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.

what you actually get

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

    ~/.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
nothing up our sleeve

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
3 host paths in reach
sandbox-cli --dry-run
docker run --init --rm -it \
--hostname sandbox \
--user sandbox \
--security-opt no-new-privileges \
--cap-drop ALL \
--cap-add NET_ADMIN \
--pids-limit 1024 \
--mount type=bind,source=~/projects/app,target=/workspace \
--mount type=bind,source=~/.config/sandbox/agents/claude,target=/sandbox/home \
--mount type=bind,source=~/.claude/projects/-Users-dev-app,target=/sandbox/home/.claude/projects/-workspace \
-w /workspace \
-e HOME=/sandbox/home \
-e SANDBOX_EGRESS_ALLOW=api.anthropic.com,registry.npmjs.org,… \
-e ANTHROPIC_API_KEY \
sandbox-base:0.0.1-9f95ae16 \
claude

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.

the same boundary, as a file

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

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

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

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

    Preferences that follow you across every project on this machine.

  3. 3Nearest .sandbox.yaml
    the project

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

  4. 4Command-line flags
    this one run

    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

The containerWhat the run is made of.
  • imagestringunset → built-in sandbox-base

    The 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 → /workspace

    Where the project is mounted and where the guest command starts.

  • usersandbox | rootunset → sandbox

    Non-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/home

    The fake HOME. Nothing under it is host-connected unless an agent wrapper persists its login there.

  • hostnamestringunset → sandbox

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

Mounts & environmentEverything that crosses the boundary is named here.
  • mountslist of { host, container, mode }unset → just /workspace

    host 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 → empty

    Literal 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 suggestion

    A 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 → none

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

NetworkThe half of the problem filesystem isolation does nothing about.
  • network.modedefault | none | allowlistunset → default

    allowlist 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 only

    Extra 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 published

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

HardeningSecure by default; every knob is here to be loosened knowingly.
  • security.no_new_privilegesboolunset → true

    Blocks setuid privilege escalation inside the container.

  • security.cap_drop / cap_addlistunset → [ALL] / none

    All 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 → 1024

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

  • security.memory / cpusstringunset → unlimited

    Opt-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 profile

    Point at your own seccomp profile when the default one blocks something you need.

What survives the runContainers are --rm; these two keys decide what outlives them.
  • cache.enabled / cache.pathsbool / listunset → false / built-in dirs

    Keeps 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 / 336h

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

the network half of the problem

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.

4 of 12 destinations refused
  • api.anthropic.comthe model the agent is running on
    Baseline
  • registry.npmjs.orgnpm install, still working
    Baseline
  • github.comgit fetch, git push
    Baseline
  • pypi.orgpip install
    Baseline
  • files.pythonhosted.orgthe wheels themselves
    Baseline
  • raw.githubusercontent.cominstall scripts
    Baseline
  • internal.registry.example.comyour private registry — added with --allow
    --allow
  • api.continue.devan agent's own config endpoint, added with --allow
    --allow
  • paste.example.netthe exfiltration a prompt-injected agent was talked into
    Denied
  • webhook.attacker.tldyour .env, POSTed somewhere else
    Denied
  • crypto-pool.examplea miner the dependency chain brought along
    Denied
  • telemetry.unknown-vendor.iophone-home nobody asked for
    Denied
Default-deny, programmed with 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.
what containment buys you

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%
then, from your normal checkout:git diff main...feature-asandbox-cli worktree commit feature-a -m "…"sandbox-cli worktree rm feature-a
observability

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

$ npm test
128 passing
sandbox-cli | mem 412MiB/7.6GiB [===-----] cpu 82% · 0m47s

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

[sandbox] · mem 412MiB · cpu 82%

after every run, interactive included — the peak summary

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

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

in the base image
$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.

prior art, honestly

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 projectBuilt-in agent sandboxesClaude / CodexDocker SandboxessbxNative OS toolsSeatbelt / LandlockCloud microVMsE2B, Daytona, …
Isolation strengthHow hard the wall actually isGood — Docker + hardening, optional gVisor/KataMedium — OS-level, shared kernelExcellent — microVM / FirecrackerGood — kernel primitivesExcellent — microVMs
Local, no cloudYour code never leaves the machineYesYesYesYesNo
Persistent agent authLog in once, not every runExcellent — dedicated persistent homeVariesGoodVariesVaries
Package cache persistenceNo cold npm install every runYes — --cache volumesLimitedGoodManualOften built-in
Parallel agents (worktrees)Several branches at once, no collisionsExcellent — built-in --worktreePoorGoodPoorVaries
Credential brokerSecrets off the argv and out of historyExcellent — file / cmd / env sourcesBasicGood — proxyVariesGood
Egress / network controlStop exfiltration, keep installs workingStrong — allowlist with baselinesBasicStrongVariesStrong
Observability / metricsWhat is this thing actually doingExcellent — live gauge, stats, summariesLimitedGoodPoorVaries
Project configPer-repo policy, checked inExcellent — .sandbox.yamlLimitedGoodPoorAPI / config
Dry-run / previewRead the boundary before trusting itYesNoVariesNoVaries
Ease of useHigh — CLI-focused, thorough docsHighHighMediumMedium — setup
Cross-platformGood — macOS / Linux / WindowsGoodExcellentPlatform-specificN/A
Docker dependencyYesNoYesNoNo
Best forLocal multi-agent workflows, ergonomicsQuick minimal protectionStrongest local isolationLightweight, zero depsScale & 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).

Six axes, all “higher is better”, scored 0–5
  • 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.

platform support

Everywhere Docker runs

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

CapabilitymacOSDocker DesktopLinuxnative DockerWindowsDocker Desktop / WSL2
run, agent wrappers, mounts, env, hardening, metricssupportedsupportedsupported
--cache, --secret, --worktree, --git, --sharesupportedsupportedsupported
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 verifiedsupportedpartly verified
--host-gatewayhost.docker.internal resolves automatically on Docker Desktop; native Linux needs the flag.autoneededauto
/workspace file ownershipOn native Linux, use --user "$(id -u):$(id -g)" if ownership matters.virtualized to youcontainer uidvirtualized 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 availablesupportednot available
get started

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.

v0.0.1beta.5latest release
all 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.