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

PlatformMechanismNotes
macOSSeatbelt (sandbox-exec) with a generated profileBuilt into macOS. Nothing to install
Linuxbubblewrap (bwrap) with a generated argument listInstall bubblewrap from your package manager
WindowsNot available yetShell 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

AllowedBlocked
WriteThe workspace roots (your working directory and any additional directories), the per-user temp directory and /tmpEverything else. Inside the workspace, git configuration and hooks, and agent and editor configuration, stay read-only
ReadEverything elseSecret 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
NetworkNothingAll 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:

~/.cusp/settings.json
{
  "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:

~/.cusp/settings.json
{
  "sandbox": {
    "network": {
      "allowedDomains": ["registry.example.org", "*.example.com"],
      "deniedDomains": ["uploads.example.com"]
    }
  }
}
  • An entry is an exact host, a subdomain wildcard (*.example.com matches api.example.com but not example.com), * for any host, or an IP address, each with an optional :port.
  • deniedDomains wins over allowedDomains.
  • 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.

KeyTypeMeaning
sandbox.enabledbooleanRun shell commands in the sandbox (default true)
sandbox.failIfUnavailablebooleanRefuse shell commands when the sandbox can't be applied (default true). false runs them unsandboxed
sandbox.filesystem.allowWritestring[]Extra writable paths
sandbox.filesystem.allowReadstring[]Exceptions to the read deny list
sandbox.filesystem.denyReadstring[]Extra paths that may not be read
sandbox.network.allowLocalPortsnumber[]Localhost ports commands may bind and connect to
sandbox.network.allowLocalBindingbooleanOpen localhost entirely (default false)
sandbox.network.allowedDomainsstring[]Hosts reachable through the filtering proxy
sandbox.network.deniedDomainsstring[]Hosts that are always refused
sandbox.network.allowUnixSocketsstring[]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: false and failIfUnavailable: false
  • allowUnsandboxedCommands and excludedCommands
  • allowLocalBinding, allowLocalPorts, allowUnixSockets and allowAllUnixSockets
  • filesystem.allowRead and filesystem.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 bubblewrap package).
  • 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.denyRead to hide them.
  • Long-running processes started with Process stay sandboxed for their whole lifetime, including children.
  • On Linux, the domain allowlist is advisory (see above).

On this page