The OS Sandbox
Caliban can wrap every subprocess spawned by the Bash tool in an OS-level sandbox that restricts what the child process may do — independent of permission rules. Where permission rules decide whether a command runs, the sandbox controls what it can access once it does.
The sandbox is implemented by the caliban-sandbox crate (ADR 0032). It is disabled by default and must be explicitly enabled in settings.
The --workspace fence (what you actually get)
The one sandbox policy caliban ships and applies for you is the workspace fence. It turns on whenever you pass --workspace <path> or --restrict-paths, and it wraps Bash commands. A plain interactive caliban run has no sandbox at all.
What the fence guarantees (ADR 0054):
| Posture | |
|---|---|
| Writes | Confined to the workspace + temp dirs |
| Network | Blocked. Loopback still works, so localhost test/dev servers are fine |
| Reads | Open — the whole host, including ~/.ssh and ~/.aws/credentials |
| Environment | Scrubbed. Secret-named variables (*KEY*, *SECRET*, *TOKEN*, *PASSWORD*, *CREDENTIAL*, plus OTEL_EXPORTER_OTLP_HEADERS) are dropped from the command's env, so ANTHROPIC_API_KEY and the like are invisible to what the model runs |
A sandboxed command can read your credential files. It cannot send them anywhere, because egress is blocked. These two facts are load-bearing together: open reads are safe only while the network is shut.
If you re-open the network with --sandbox-network=allow, you also re-open the
credential-exfiltration path. Use it deliberately, on runs you trust.
The sandbox is a write fence plus an egress block — not a read jail, and not a secrets boundary against a command that has network access by some other route.
"Loopback still works" means different things per platform.
On Linux, the sandboxed command gets its own network namespace — its
loopback is private, and it cannot reach anything listening on your host's
127.0.0.1.
On macOS, Seatbelt does not virtualize the network, so a sandboxed command can reach services on your host's loopback — a local database, an admin UI, a dev server. In particular, if you run a local forward proxy, a sandboxed command can reach the internet through it, routing around the egress block.
If you run local services you would not want a hijacked command to touch, do not rely on the macOS fence alone.
Letting a sandboxed run reach the network
git fetch, cargo against crates.io, npm install, gh, and curl all need egress, so they fail inside the fence by default. A command that fails while egress is blocked will tell you so. To opt out:
caliban --workspace ./repo --sandbox-network=allow
or, persistently, in settings.json:
{ "sandbox": { "network": "allow" } }
The CLI flag wins over settings; settings win over the default (deny).
Per-hostname allowlists (e.g. "allow github.com only, so the agent can open a PR") are not yet supported — neither sandbox backend can filter egress by hostname, so it requires a proxy. Tracked in #477. Until then the opt-out is all-or-nothing.
```admonish note title="The [sandbox] TOML table below is not wired"
The policy reference that follows describes the caliban-sandbox crate's full
Policy surface. Only sandbox.network in settings.json is currently read by
caliban; the other knobs are reachable from the library but not from a config
file. Treat the rest of this page as a description of the sandbox engine, not
of settings you can set today.
## Platform support
| Platform | Backend | Status |
|----------|---------|--------|
| macOS | Apple Seatbelt (`sandbox-exec`) | Supported |
| Linux / WSL | bubblewrap (`bwrap >= 0.5`) | Supported |
| Windows native | — | Not supported in v1; use WSL for the bubblewrap backend |
```admonish warning title="Seatbelt deprecation"
Apple has deprecated the `sandbox-exec` / Seatbelt API. It still ships in all
current macOS releases, but caliban's macOS backend will need to move to the
Endpoint Security Framework if Apple removes `sandbox-exec` in a future OS
version. There is no announced removal date.
Enabling the sandbox
Add a [sandbox] block to your project or user settings.toml:
[sandbox]
enabled = true
fail_if_unavailable = true # refuse to start if bwrap/sandbox-exec is missing
With fail_if_unavailable = false (the default), caliban falls back to running unsandboxed if the backend binary is absent or too old, and logs a warning.
What the sandbox restricts
The sandbox limits three classes of access for spawned subprocesses:
Filesystem
| Key | Effect |
|---|---|
filesystem.allow_read | Paths the subprocess may read |
filesystem.deny_read | Paths hidden from reads (shadows an allow_read entry) |
filesystem.allow_write | Paths the subprocess may write |
filesystem.deny_write | Paths write-denied within an allow_write root |
On Linux, denied paths are masked with --tmpfs (an empty in-memory directory shadows the real one). On macOS, Seatbelt uses (deny file-write* (subpath …)) rules. Glob patterns are not supported in filesystem ACLs — add explicit path roots.
The environment variables ${WORKSPACE}, ${HOME}, and the XDG vars are expanded when the sandbox is initialized.
Network
Per-hostname egress is not reliably enforceable by either backend alone. The supported patterns are:
- Block all egress — leave
network.allowed_domainsempty. Uses--unshare-neton Linux and omits allnetwork-outboundallow rules on macOS. - Proxy-filtered egress — set
network.http_proxy_portto route subprocess HTTP through an operator-run proxy at127.0.0.1:<port>. The proxy enforces domain rules; the sandbox only allows the loopback port.
If you set allowed_domains to a non-empty list on Linux without also
configuring http_proxy_port, caliban logs a warning: the Linux bubblewrap
backend cannot enforce per-hostname rules without a proxy layer.
macOS Seatbelt supports literal (remote tcp "host:port") rules and is
correspondingly stricter.
Other network settings
[sandbox.network]
allow_unix_sockets = false # Docker daemon socket, etc.
allow_local_binding = false # bind() on local ports
allow_mach_lookup = [] # macOS-only: Mach service names
Full configuration example
[sandbox]
enabled = true
fail_if_unavailable = true
auto_allow_bash_if_sandboxed = true
allow_unsandboxed_commands = ["git", "gh"]
enable_weaker_nested_sandbox = false
[sandbox.filesystem]
allow_read = ["${WORKSPACE}", "/etc", "/usr"]
deny_read = ["${HOME}/.ssh"]
allow_write = ["${WORKSPACE}"]
deny_write = ["${WORKSPACE}/.git/hooks"]
[sandbox.network]
http_proxy_port = 8888
allow_unix_sockets = false
allow_local_binding = false
Key settings
auto_allow_bash_if_sandboxed — When both enabled and this flag are true, the permission classifier auto-allows all Bash(*) calls without showing a prompt. The sandbox is the protection; the Ask modal becomes redundant. Defaults to false. Note: commands listed in allow_unsandboxed_commands are not auto-allowed — they run outside the sandbox and still go through normal permission rules.
allow_unsandboxed_commands — A glob list matched against the first token of each command (or the full command string when the pattern contains a space). Matching commands bypass the sandbox entirely. Use this for tools that genuinely need unrestricted access — for example, git or gh.
enable_weaker_nested_sandbox — For dev containers or VMs that are already inside a user namespace: drops the --unshare-user flag on Linux (which would otherwise fail). This is a no-op on macOS.
bwrap_path / sandbox_exec_path — Override the path to the sandbox binary if it is not at the default location ($PATH for bwrap; /usr/bin/sandbox-exec for macOS).
How it works
SandboxedShim::wrap_command intercepts the tokio::process::Command built by BashTool before it is spawned. If the sandbox is active and the command is not on the bypass list, it rewrites the command so that:
- On macOS:
sandbox-exec -f <profile.sb> <original command> - On Linux:
bwrap [bind/ro-bind/tmpfs flags] <original command>
The rest of the Bash tool — stdout/stderr capture, PID-group cleanup, timeouts, cancellation — is unchanged. The sandbox is a shim layer, not a fork.
Detection runs at startup. bwrap version >= 0.5 is required on Linux (the --die-with-parent flag arrived in 0.5).