Tech

Sandboxing Claude Code on macOS: Stop Yolo Mode Without Losing Power

Akshay Sarode
Direct answer

Wrap every Claude Code spawn in sandbox-exec -f <profile> with a Seatbelt profile generated from capability tokens. Default policy: read OS/runtime paths plus the workspace, write only inside the workspace/temp, network on, subprocess on. Grant extra capabilities (screen recording, Accessibility, FDA) per-agent. Spawn decisions, grants, toggles, and lifecycle incidents are logged; per-deny Seatbelt ingestion is planned.

The first time you let Claude Code do git reset --hard on the wrong branch, you become permission-curious. The second time, you become sandbox-curious. The Anthropic permission prompt is great but it has two failure modes: you click "yes" without reading because you're tired, or you toggle --dangerously-skip-permissions because the prompts annoyed you that day.

macOS has a kernel-enforced sandbox available to every binary. It's called Seatbelt and it's been there since 2007. Apple uses it inside app sandbox. You can use it directly with sandbox-exec. This post is the production version of "I read the man page and built something useful."

Why people skip permissions (and what it costs)

--dangerously-skip-permissions exists because the prompt-on-every-tool-call gets old. The cost is real: an agent decides it needs to clean up node_modules across the whole filesystem, types the wrong path, and you have a bad evening. Sandbox prevents the worst outcome regardless of what the agent decides.

Seatbelt 101

A Seatbelt profile is an S-expression. You declare a default (allow or deny), then add specific allow/deny rules per operation. sandbox-exec -f profile.sb claude runs Claude Code under those rules. The kernel enforces them.

(version 1)
(deny default)
(allow process-fork)
(allow process-exec*)
(allow file-read* (subpath "/System"))
(allow file-read* (subpath "/usr"))
(allow file-read* file-write* (subpath "/Users/me/BernatoAgents"))
(allow file-write* (subpath "/tmp"))
(allow network*)
(allow mach-lookup
  (global-name "com.apple.system.notification_center"))

That profile reads OS/runtime paths plus the workspace, writes only in the workspace + /tmp, runs sub-processes (so git, npm, etc. work), and has network. Drop the network line and you've got an offline agent. Drop subprocess and Claude can read files it is granted but can't run anything.

The capability-token model

Hand-writing Seatbelt profiles per agent is brittle. Capability tokens make it composable. Each token is a string with a meaning:

The default bundle is {workspace_rw, network, subprocess}. That's the right policy for most agents: they can read/write the workspace, run tools, talk to the internet, and not wander through the rest of your filesystem unless you add a read:<path> or read_anywhere grant.

Per-agent grants

A single config file (~/.bernato_capabilities.json in our daemon's case, but the pattern is generic) lists grants:

{
  "grants": [
    {
      "match": { "agentNamePattern": "screenshot-*" },
      "capabilities": ["screen_capture"]
    },
    {
      "match": { "command": "claude --dangerously-skip-permissions" },
      "capabilities": ["privileged"]
    }
  ]
}

Match block is AND-within-grant; OR-across-grants. An agent named screenshot-friday gets the default bundle plus screen_capture. An agent invoking --dangerously-skip-permissions still drops the sandbox — but you've consented to that explicitly, in version-controlled config, instead of by mashing y/y/y at 11pm.

The audit log

Every spawn writes a row: agent, command, capabilities resolved, profile path, exit code. Today this is a local activity log covering spawn decisions, grants, toggles, and lifecycle incidents; tamper-evident chaining, per-deny Seatbelt ingestion, and an external verifier are roadmap work.

Practically, this means: when something weird happens, you can answer "did the sandbox engage?" with a query, not a guess.

Restart-to-apply for TCC permissions

macOS caches some TCC grants per-process. Granting Screen Recording or Full Disk Access mid-session won't flip to "granted" in your daemon until the daemon is restarted. The daemon should detect this and offer a restart button (we do) — and persist the user's wizard step so the wizard resumes on the right screen after re-exec.

What this doesn't stop

Sandbox is a hard barrier on filesystem and network. It does not stop the agent from making bad commits, leaking secrets in git diff, or being prompt-injected into doing something silly with the capabilities you did grant. Sandbox is a containment story, not a behavior story.

Linux equivalent

For Linux, the target equivalent is bubblewrap + namespaces, Landlock/seccomp, or a similar kernel boundary. The capability-token concept maps cleanly, but Linux enforcement is roadmap work; macOS is where the daily-driver dev box lives so we shipped and polished Seatbelt first.

What we ship

This is exactly how Bernato spawns every agent. The default-on sandbox is the v2 daemon's posture. The user can flip the master switch off via the tray ("Sandbox: OFF") for an unrestricted run; the audit log shows when that happened and which agents ran during. Per-agent grants live in ~/.bernato_capabilities.json.

FAQ

Does this work for Cursor too?

Yes — Cursor's terminal-spawned tooling runs under the same sandbox if you launch Cursor's tools through the daemon. Cursor's IDE itself isn't sandboxed (it needs broad access for editor features), but the agent processes it spawns can be.

Does sandbox-exec slow Claude Code down?

Negligibly. Seatbelt is kernel-implemented and the per-syscall check is sub-microsecond. The startup cost is one fork + profile compile (~5ms).

What about network in sub-shells?

Sub-shells inherit the sandbox profile. If the agent runs npm install, that npm gets the same sandbox. Drop the network capability and the install fails — same as if you'd unplugged the cable.

Can I exfiltrate data through DNS?

Yes if network is allowed. Sandbox doesn't inspect packet contents. Pair sandbox with egress firewalls (Little Snitch, pf rules) if exfiltration is in your threat model.

Where's the tamper-evident audit code?

Not shipped in Bernato yet. The daemon writes local activity logs today; tamper-evident chaining and external verification are roadmap work.