Skip to content

Sandbox and security

An agent reads files, runs shell commands and calls external APIs on your behalf. The failure you care about is rarely malice — it is an agent that misreads a path and rewrites your working checkout, or one that pushes a branch you had not finished reviewing. So the question the product has to answer is not “is the agent trustworthy” but “what is the blast radius when it is wrong”.

Control Center answers that in layers and the layers are independent on purpose. If one is missing, the others still hold. That matters, because the outermost layer — the OS sandbox — is the only conditional one: it depends on the host offering a backend and on Windows no host does.

Five things bound an agent run on every host, sandbox or no sandbox. None of them is a kernel boundary; all of them are real. Where the host offers a backend the OS sandbox sits above them and “What the sandbox adds and where it stops” below covers what that changes.

Its own copy-on-write worktree. Every conversation provisions its own checkout of each linked repo, cloned copy-on-write through the bundled rift native rather than copied byte by byte, so isolation is cheap enough to be the default. The registered repository you added to the workspace is not the directory the agent works in — two conversations touching the same repo cannot see each other’s edits and neither is editing your checkout.

The grain is worth being precise about: the worktree is per conversation, not per agent. Two agents in the same conversation get distinct overlay working directories that share that conversation’s repos/ — which is the point, since they are collaborating on one change — while their private agent directories stay separate. Copy-on-write is the only backend wherever rift ships: a filesystem that cannot reflink, or a data dir on a different volume from the repo, fails the provision rather than falling back. Windows is the exception — there is no copy-on-write backend there, so git worktree is the backend, not a silent degradation.

Path confinement in the built-in harness’s file tools. The harness’s read, write, edit, apply_patch, search, find and search_files tools resolve every path against the run’s working directory plus shared roots — the conversation’s repos/ directory, where the worktrees live, and the space’s attachments dir when it exists — and refuse anything that lands outside. Absolute paths are accepted only if they were already inside. A symlink inside the worktree pointing out of it is resolved and refused too. This is why an agent cannot write to ~/.ssh even with no sandbox in the picture.

The mode’s tool surface. A conversation’s mode decides which tools are materialised for the run at all. In the read-only modes (plan, review, orchestrate) the mutating tools are simply not in the registry and the shell command policy for those modes denies every mutating command outright — git commit, git push, npm install, docker and the rest — rather than prompting for them. A tool that is not present cannot be called, which is a stronger guarantee than a tool that is present and gated.

The unified action guardrails. Every mutating tool declares the effect classes it can produce and the policy engine resolves each one to allow, prompt, or deny before the call runs. A prompt with no approver connected resolves to denied — a question nobody can answer is never a yes. See Guardrails for the resolution model.

Brokered credentials and a sanitised environment. Credentials are minted per launch and revoked when the run ends, and the GitHub token a run holds can read but cannot push — see “Pushes go through Control Center” below. ACP, harness bash and Claude Code under the native wrap get an environment stripped of the dynamic-linker and interpreter injection vectors (LD_PRELOAD, DYLD_INSERT_LIBRARIES, NODE_OPTIONS, GIT_SSH_COMMAND and their kin) before the process starts. Claude Code through NoSandboxAdapter inherits the host environment.

The harness’s file tools run in the server process, not in a child process, so no sandbox profile could constrain them even if one were attached — only bash goes through the sandboxed command runner. Their confinement is the path check described above and that check is therefore load-bearing rather than a second line of defence.

The bash tool is the other place to be precise. Its commands are evaluated against the mode’s command policy and then spawned with a sanitised environment and the worktree as its working directory. Where a backend exists that spawn is wrapped, so the sandbox is what stops a shell command from writing outside the worktree; where none does — Windows, or Linux without bwrap and socat — the command policy is the only thing between the command and the rest of the disk. In chat mode the policy hard-denies a fixed list (privilege escalation, system and disk destruction, container and namespace escape) and prompts on remote-state mutations (git push, gh pr create, package publishes and installs). Anything on neither list runs. Pipelines, chains and subshells are parsed and each sub-command evaluated separately, with the most restrictive decision winning.

Claude Code, when used as an adapter, is the case where the sandbox carries the most weight. Control Center launches it with --dangerously-skip-permissions, because a non-interactive claude -p would otherwise block forever on its own approval prompt. Its read, write and edit calls happen inside its own process and Control Center never sees them. Its Bash calls are sent, before they run, to a hook on the server that classifies the command line and asks the matching rule, and the mcp__* tools Control Center serves are gated as usual. Where a backend exists, those unseen calls still happen inside the sandbox wrap, so the OS is the boundary around them. Where none does, nothing is. Each adapter states this in its enforcement matrix — see the adapter honesty matrix.

There are no per-agent permission flags. Whether an agent may commit, push, open a pull request, write to a tracker or reach the network is the guardrails’ call — allow, ask first or deny — and the same rules apply whichever adapter runs the agent.

A push is the action that has to hold even against an agent working around a rule, so it is enforced by what the agent holds rather than by what it runs. With a GitHub App configured, a run in a GitHub worktree gets a repo-scoped installation token that can read contents and work with pull requests but cannot push. The run’s git configuration rewrites every GitHub push URL (https://github.com/, git@github.com:, ssh://git@github.com/) to the agent run gateway, a loopback endpoint on cc_server. The gateway reads which refs the push updates, asks the Push to a remote rule — so a branch-constrained rule such as “deny pushes to main” applies — and only then forwards the push to GitHub on a write token it mints for that purpose and never hands to the agent. A refused push shows up in git’s own output:

! [remote rejected] main -> main (Control Center policy: …)

The gateway forwards pushes only to the worktree’s own repo or a GitHub repo linked to the workspace. Fetches go straight to GitHub on the read token.

Other shell commands meet a softer gate. The harness bash tool, and Claude Code’s Bash tool through a hook, classify each command line and ask the rule it falls under: git commit, merge, rebase, cherry-pick, revert and am are Create a commit; gh pr create is Open a pull request; gh pr merge, review, comment and close are Publish a review or merge; gh issue create, edit, comment and close are Write to an external tracker; package installs are Install a package; curl and wget are Access the network. This recognises the conventional spelling of an action, and an agent determined to hide one (eval, a script file, a raw API call) can. Pushes do not depend on it.

No ticketing API key is placed in a sandbox. Agents write tickets through Control Center’s MCP tools, which ask Write to an external tracker.

The Access the network rule is read once, when the run starts. A sandbox’s network is either on or off, so a deny turns it off while allow and ask first leave it on; the harness’s web_fetch and web_search tools still ask per call. Credentials are revoked when the run tears down, together with any write token the gateway minted, so a handle does not outlive the run.

The SandboxBackend enum has three values — native (Seatbelt on macOS, bubblewrap on Linux and WSL2), none, and microvm. Agent command wrapping uses only the first two: microvm is the enclosed-VM backend for interactive terminals and rigs, not a silent upgrade of a bash wrap. Understanding the native shape explains what you gain where one is attached and what it was never going to give you.

cc_server probes the host once at boot and where a backend answers it feeds both of the seams the dispatch path has, because the transports do not share one. The first (SandboxPort) wraps the Claude Code CLI. The second (SandboxManager) wraps ACP adapters and the built-in harness’s bash tool. Feeding only one would leave the others spawning unwrapped, which is what used to happen. On a host with no backend, or under --sandbox off, they all spawn unwrapped and the server says which case you are in on its startup line. ACP and harness bash still sanitise the environment; Claude Code through NoSandboxAdapter inherits the host environment.

One surface deliberately opts out: the server-hosted terminal accepts a backend argument per session and cc_server constructs it defaulting to the host shell, so a terminal you open from the app runs unsandboxed unless a client names native explicitly. That is a real shell on the server host, not an agent run.

On Linux, bubblewrap builds a mount namespace, so the agent genuinely sees only what was bound into it. On macOS, sandbox-exec applies a Seatbelt profile that is permissive by default — a fully deny-by-default profile makes an interactive shell unusable, because the number of dyld, XPC and Mach calls to enumerate is impractical. The macOS profile therefore starts from (allow default) and carves out denies: reads of secret paths, a write policy reset to deny with explicit allows on top, unlink-blocking on every denied path and its ancestors so a rename cannot bypass a write-deny, exec denies for always-dangerous binaries and for anything copied into a writable directory and network restricted to the loopback proxy ports plus DNS.

The practical consequence: on Linux the agent’s view of the filesystem is narrowed; on macOS the agent can still read most of the host and your registered checkouts are protected by explicit write and rename denials rather than by invisibility. Both are namespace-level isolation, not a kernel security boundary — the backends say so in their own capability notes.

The bind-mount set is where the cross-agent boundary is drawn. Overlay dispatch (Claude Code and ACP) mounts up to four paths, resolved per run: the agent’s overlay working directory read-write, the agent’s config directory read-only, the conversation’s shared repos/ directory read-write when it exists, and the space’s attachments directory read-only when it exists. A sibling agent’s directory is never mounted. The harness bash wrap is narrower: it bind-mounts only the run’s working directory. This is the same shape the harness’s in-process path check enforces in software, so the two agree — but only the path check covers the harness’s own file tools, which never reach a mount namespace at all.

Your registered checkouts join the deny-write set on top of that, in every mode and on all three of the wrapped paths (Claude Code, ACP, harness bash), and each rule is emitted in both its literal and its symlink-resolved spelling. The second spelling is not a nicety: the kernel matches a deny rule against the resolved path and macOS makes resolution the common case rather than the exotic one — /tmp is really /private/tmp, /var/folders/… is really /private/var/folders/… — so a checkout reached through either would have been writable while the profile appeared to forbid it.

Network, under a sandbox, is a per-session domain allowlist enforced by in-process loopback proxies. Egress is default-deny: with the network off, the proxies still start but their allowlist is empty, so every outbound connection is refused. With it on, the resolver builds the allowlist from a curated baseline — LLM API providers, git hosting, package registries, the model catalogue — plus GitHub’s domains and matches every connection’s target host against it. Wildcards work (*.example.com matches subdomains, not the apex). A separate always-denied list wins over the allowlist and covers cloud metadata endpoints and telemetry sinks. What is not configurable is the list itself: the spec carries an egressAllowlist field for extra domains, but nothing in the dispatch path populates it, so in practice every run gets the baseline plus GitHub and nothing more.

Denials, on macOS, are observable: a monitor taps the system log for Sandbox denials, filters known noise, deduplicates retry storms and surfaces each as a SandboxViolation naming what was attempted and what was blocked. On Linux the wrap path parses stderr for EPERM, Operation not permitted and Permission denied and emits the same event type — there is no system-log tap. Those arrive on the run’s own event stream, so a denied syscall shows up in the transcript rather than only in a log. The telemetry only exists while a native sandbox is actually wrapping the process — on a host with no backend there is nothing to observe. It is also unrelated to the guardrails: a violation comes from a denied syscall, never from a rule lookup.

Confirmations come from the guardrails, not the sandbox

Section titled “Confirmations come from the guardrails, not the sandbox”

The sandbox has no approval path. It launches, executes, emits events and tears down; there is nowhere in it for a human to answer a question. When you see a confirmation in a space — approve this push, approve this delete — it was raised by the guardrail engine (or the harness’s command policy) before the action reached the runtime at all.

This ordering is deliberate. An approval that happened after the syscall would be an audit record, not a gate. It is also why the fail-closed rule lives in the guardrails: with no approver connected, the decision is made before anything runs and it is a denial.

Run agent diagnostics is the single place that states what can be checked by hand.

  • Rigs and enclosures: the step above the sandbox — when the work needs a whole disposable machine with a kernel boundary, not a wrapped process
  • Guardrails: the policy layer above the OS floor and where approvals actually come from
  • Agent dispatch lifecycle: where isolation fits in the dispatch flow
  • Modes: how a mode narrows the tool surface and the command policy
  • Sandbox backends reference: the backends and their configuration