Sandbox
Shell commands the agent runs are contained by the operating system sandbox — Seatbelt on macOS, bubblewrap on Linux — with workspace-only writes, no credential reads and no network unless you allow it.
Permissions decide whether a command may run. The sandbox limits what a command can do once it runs. The sandbox is on by default: a shell command the agent starts can write only inside your workspace and temp directories, can't read your credential stores, and has no network access, not even to localhost, unless you allow it.
The two layers work together. A sandboxed command is contained even if it does something you didn't expect, which means you can approve more commands without reading each one closely.
How it works
| Platform | Mechanism | Notes |
|---|---|---|
| macOS | Seatbelt (sandbox-exec) with a generated profile | Built into macOS. Nothing to install |
| Linux | bubblewrap (bwrap) with a generated argument list | Install bubblewrap from your package manager |
| Windows | Not available yet | Shell commands are refused unless you opt out. See Failing closed |
The sandbox applies to shell commands (Bash, Process and Terminal) and to helpers Cusp starts for the session, such as type checkers and language servers. Cusp's own file tools (Read, Edit, Write, …) don't go through it; they are governed by permissions and protected paths.
Sandboxed commands also run with a cleaned environment: provider keys and Cusp's own credentials are removed.
The default policy
| Allowed | Blocked | |
|---|---|---|
| Write | The workspace roots (your working directory and any additional directories), the per-user temp directory and /tmp | Everything else. Inside the workspace, git configuration and hooks, and agent and editor configuration, stay read-only |
| Read | Everything else | Secret stores in your home directory: ~/.ssh, ~/.aws, ~/.gnupg, ~/.azure, ~/.kube, ~/.config/gcloud, ~/.docker/config.json, ~/.netrc, ~/.password-store, ~/Library/Keychains, CLI tokens (~/.config/gh, ~/.git-credentials, ~/.pypirc, ~/.cargo/credentials, ~/.vault-token, …), browser profiles, container runtime sockets, and agent data directories (~/.cusp, ~/.claude, ~/.codex, ~/.gemini) apart from the skills inside them |
| Network | Nothing | All network access, including localhost |
Paths are resolved to their real locations before the profile is built, so a symlink can't be used to escape (on macOS, /tmp becomes /private/tmp).
Opening up the network
Local ports
Dev servers, local databases and test suites often need localhost. Allow the ports they use:
{
"sandbox": {
"network": {
"allowLocalPorts": [3000, 5432]
}
}
}allowLocalBinding: true opens localhost entirely. The daemon's own port is never opened.
Domains
To let commands reach specific hosts, for example a package registry, list them in allowedDomains. Traffic then goes through Cusp's filtering proxy:
{
"sandbox": {
"network": {
"allowedDomains": ["registry.example.org", "*.example.com"],
"deniedDomains": ["uploads.example.com"]
}
}
}- An entry is an exact host, a subdomain wildcard (
*.example.commatchesapi.example.combut notexample.com),*for any host, or an IP address, each with an optional:port. deniedDomainswins overallowedDomains.- A request to a host on neither list prompts you: allow once, allow for the session, or deny.
- Private and local addresses are reachable only through an explicit host or IP entry.
- A refused request gets a 403 response whose body starts "Blocked by Cusp network policy:", so the agent can tell you which host it needs.
On Linux, the domain allowlist is advisory for now. Programs are pointed at the proxy through HTTP_PROXY and related variables, but there's no network namespace isolation yet, so a program that ignores proxy settings can reach the network directly. On macOS the sandbox profile enforces it. Cusp shows this gap as a notice when the session starts.
Settings
Sandbox settings use the common sandbox settings schema, so an existing .claude/settings.json sandbox block works as is.
| Key | Type | Meaning |
|---|---|---|
sandbox.enabled | boolean | Run shell commands in the sandbox (default true) |
sandbox.failIfUnavailable | boolean | Refuse shell commands when the sandbox can't be applied (default true). false runs them unsandboxed |
sandbox.filesystem.allowWrite | string[] | Extra writable paths |
sandbox.filesystem.allowRead | string[] | Exceptions to the read deny list |
sandbox.filesystem.denyRead | string[] | Extra paths that may not be read |
sandbox.network.allowLocalPorts | number[] | Localhost ports commands may bind and connect to |
sandbox.network.allowLocalBinding | boolean | Open localhost entirely (default false) |
sandbox.network.allowedDomains | string[] | Hosts reachable through the filtering proxy |
sandbox.network.deniedDomains | string[] | Hosts that are always refused |
sandbox.network.allowUnixSockets | string[] | Unix sockets commands may connect to |
These keys are accepted so existing settings load, but Cusp doesn't apply them yet: autoAllowBashIfSandboxed, allowUnsandboxedCommands, excludedCommands, filesystem.denyWrite, network.allowAllUnixSockets, network.httpProxyPort and network.socksProxyPort. Unsandboxed runs always need your approval, and no command is excluded from the sandbox.
Who can change what
A repository must not be able to switch off its own sandbox. Settings that weaken it are honored only from your user settings, managed settings or --settings, and ignored, with a warning, in a project's .cusp/ or .claude/ settings:
enabled: falseandfailIfUnavailable: falseallowUnsandboxedCommandsandexcludedCommandsallowLocalBinding,allowLocalPorts,allowUnixSocketsandallowAllUnixSocketsfilesystem.allowReadandfilesystem.allowWrite
A project can still make its sandbox stricter, for example with filesystem.denyRead or network.deniedDomains.
When a command is blocked
Sandbox failures are hard to diagnose from a bare Operation not permitted. When a sandboxed command fails, Cusp looks at the error and the exit code and tells the agent which limit most likely caused it: a write outside the workspace, a read of a protected credential store, or a network request to a named host. The agent can then work within the limit or explain what it needs.
If the agent decides it genuinely needs to run a command outside the sandbox, it sets dangerouslyDisableSandbox on the Bash call. That always requires your approval; no rule, mode or classifier can grant it.
Failing closed
If the sandbox can't be applied, because bwrap isn't installed, sandbox-exec is missing, or the platform has no sandbox (Windows), Cusp doesn't silently run commands unsandboxed. It refuses shell commands and shows a notice when the session starts. You can then:
- Install the sandbox (on Linux, the
bubblewrappackage). - Approve single commands to run unsandboxed when the agent asks.
- Set
"sandbox": { "failIfUnavailable": false }in your user settings to run shell commands unsandboxed, with the permission engine as the only layer.
The same rule applies to every safety layer in Cusp: a check that can't run denies or asks, never allows.
Audit
Every permission decision and every connection through the network proxy is recorded in <data directory>/audit/<session>.jsonl, readable only by you.
Limits
- The sandbox constrains the processes the agent starts. It doesn't constrain the daemon itself or Cusp's file tools.
- Commands you approve to run unsandboxed are not contained.
- Reads are broad by design. Anything readable outside the deny list, such as source in other folders, can be read by a sandboxed command. Add paths to
filesystem.denyReadto hide them. - Long-running processes started with
Processstay sandboxed for their whole lifetime, including children. - On Linux, the domain allowlist is advisory (see above).