OS sandbox
An optional OS-native sandbox around a [step](glossary.md#step) process: the user's configured Claude Code or Cursor CLI (and everything they start), built-in steps, and custom command steps.
An optional OS-native sandbox around a step process: the user's configured Claude Code or Cursor CLI (and everything they start), built-in steps, and custom command steps.
Inside it a step can write only its worktrees, the git folders it needs, its artifacts folder, and a private temp folder. It cannot read the user's home folder, keys, other repos, the secret store, or the signing agent. Seatbelt on macOS 14+ (sandbox-exec), Bubblewrap on Linux and WSL2. Native Windows has no backend.
Default is off. A factory that never sets sandbox keeps today's argv.
See also: What a step can reach, Capability matrix, kusaw sandbox check / kusaw sandbox doctor.
What it protects
- Writes stay in the attempt's worktrees, needed git object/ref folders, the attempt folder, and a private temp folder (
TMPDIR,XDG_CACHE_HOME,XDG_RUNTIME_DIR). - Git control files (
config,hooks/, and related) are mounted read-only (GF-89). The factory's own git stays outside the sandbox. - The real home,
~/.ssh,~/.gnupg, the GF-84 store and key folders, and (on WSL)/mntand/run/WSLare not visible.WSL_INTEROPis unset. - Signing stays outside: the signing shim,
~/.gnupg, and SSH agent sockets are not visible inside. - On Linux and WSL the D-Bus session socket is not mounted. On macOS the Keychain Mach services are denied to every sandboxed step.
- Linux and WSL run the step in new user, PID, IPC and UTS namespaces with
--die-with-parentand--new-session.
What it doesn't protect
- Kernel escapes: a shared-kernel process sandbox (about as strong as a container on Linux; weaker than a VM on macOS).
network: open(the default) leaves the network, including services on the host'slocalhost(a dev server, a local database), unrestricted. Capabilitynetworkstaysnot_supporteduntil the factory opts intoallowlist.- Login tokens or login folders an agent CLI must hold itself stay inside the sandbox (Q98). Only API keys for clients that accept a custom endpoint are kept out via
inject. - Tools that ignore
HTTP_PROXY/HTTPS_PROXYfail closed in allowlist mode (kusaw sandbox doctorlists them as W1). - macOS process listing: the profile blocks
psand signals to other processes, but there is no PID namespace. Paths outside the sandbox can be checked for existence, size and dates, not read. - macOS detached processes: a process that calls
setsidsurvives the attempt, still under the profile. - Linux and WSL: the agent's own login folders, which are mounted for it. macOS: the agent's login token, which is in that step's env.
- Anything in
sandbox.allow_readorsandbox.allow_write.
kusaw sandbox check --verbose prints this list.
Settings
sandbox:
mode: off # off (default) or required
network: open # open (default) or allowlist
allow_read: []
allow_write: []
egress:
allow: [] # host names or *.suffix, optional :port
provider_hosts: true # add each runner's EgressHosts
inject: [] # or the string provider
repos:
demo:
sandbox: required # optional; for steps whose active repos include demo
steps:
review:
sandbox: off # optional; most specific wins
build:
sandbox:
egress:
allow: [api.demo.invalid] # adds to the factory allowlistnetwork: allowlist is opt-in. A sandboxed step then reaches only allowed hosts, the run's leased local ports (later), and nothing else. Outbound traffic goes through one local HTTP proxy per serve process (CONNECT for TLS, plain HTTP). TLS is tunnelled: the proxy never decrypts and installs no CA. DNS is resolved by the proxy; private, loopback, and link-local addresses are refused unless the entry names them (localhost).
On macOS, Seatbelt allows outbound traffic only to localhost on the proxy port (and leased ports). On Linux and WSL, Bubblewrap drops --share-net and the hidden __egress-bridge forwards the proxy socket (and leased ports) onto loopback inside a private net namespace.
Every sandboxed process gets HTTPS_PROXY/HTTP_PROXY (both cases) and NO_PROXY=localhost,127.0.0.1,::1. Handshake egress: [hosts] is never auto-allowed: validate warns (W3) until the user adds them.
inject holds an API key outside the sandbox: the proxy adds the header and sets base_url_env to the local address and key_env to factory-egress-placeholder. Blocked hosts are written to the attempt's egress.log and emitted as egress.blocked (at most 50 per attempt) plus egress.summary.
kusaw sandbox doctor prints per-step mode, the allowlist with the origin of each host, injected names only, the proxy socket, and probes P5–P7. make e2e-egress is this version's probe suite.
Effective value for one attempt: the step's sandbox if set; else required if any active repo of the attempt says required; else off if all those repos say off; else sandbox.mode.
A required attempt never runs unsandboxed. kusaw sandbox check and kusaw service status report whether this machine can run the backend. kusaw validate --json includes sandbox: {backend, available, reason, fix} when any attempt could be sandboxed.
macOS: signing in the agent
On macOS each sandboxed attempt gets a fresh, empty HOME inside its private temp folder. Session files for that attempt live there and are removed with the temp folder. The real agent state folders are not mounted, and the Keychain is unreachable.
The agent signs in with a login token declared as a step secret:
- Run
claude setup-token(or create a Cursor API key). kusaw config set steps.<id>.env.CLAUDE_CODE_OAUTH_TOKEN --secret(orCURSOR_API_KEY).
Repeat for each sandboxed agent step. Probe reports the env names: Claude Code CLAUDE_CODE_OAUTH_TOKEN / ANTHROPIC_API_KEY; Cursor CURSOR_API_KEY. Missing token: validate warning, then the attempt is refused before it starts.
Linux and WSL: agent login folders
On Linux and WSL the sandbox mounts the agent's own state folders read/write (Claude Code ~/.claude/ and ~/.claude.json, or CLAUDE_CONFIG_DIR; Cursor folders from Probe). Nothing else of the home is mounted.
Agent CLIs inside the sandbox
The agent's own sandbox is turned off inside (macOS refuses nested Seatbelt; nested Bubblewrap depends on the kernel). Permission modes and presets stay. Claude Code gets --settings '{"sandbox":{"enabled":false}}'. Cursor gets --sandbox disabled only when the factory sandbox wraps the step.
What stays outside
Setup commands, the factory's own git plumbing, the signing shim, interactive intake, and serve itself. per_repo: true attempts each get their own sandbox with only that repo's worktree writable.
Statuses, outcomes, and exit codes
Run states, step outcomes, typed reason codes, and CLI exit codes, generated from the enumerations in the factory binary.
Providers
A step's [provider](../glossary.md#provider) is the agent CLI that runs it. `claude-code`, the user's configured Claude Code, is the default. `cursor` is optional. Each provider's [capabilities](../glossary.md#capability) say what a step can require.