I turned on Claude Code's sandbox two weeks ago expecting to turn it off within the hour. Two weeks in, I'm keeping it on.
If you read ~/.claude/ Is Production, you know I care about not letting AI tools run loose on my machine. Sandbox is the feature that's supposed to help with that. On macOS, it wraps every Bash tool call in sandbox-exec (Apple's seatbelt sandboxing), restricting filesystem writes and network access at the OS level. Deny-default. If you don't explicitly allow a path or host, the command fails. The config took some iteration, but the payoff is real.
Sandbox only covers Bash tool calls. Read, Edit, and Write are not sandboxed. More on that boundary later. Bash is where the real risk lives (it can run anything), but you should know the scope of what you're getting.
Also: everything here is macOS. Linux sandbox behavior may differ, and I haven't tested it.
If you just want the config, jump to the full reference.
The Minimum Viable Sandbox
Two settings in ~/.claude/settings.json. That's the starting point:
{
"sandbox": {
"enabled": true,
"autoAllowBashIfSandboxed": true
}
}
autoAllowBashIfSandboxed is the payoff: sandboxed Bash commands run without permission prompts. That's the deal. OS-level isolation in exchange for not getting asked "allow ls?" twelve times per session. We're building a global config here (in ~/.claude/settings.json), but you can also scope per-project.
One thing that tripped me up early: the sandbox auto-allows writes to the current project directory. Everything you add to allowWrite is for paths outside the project. Your code, your git operations, your build artifacts within the repo all just work.
Save those two settings, start a session, run a command. Things work... until they don't.
Filesystem: The First Thing That Breaks
The first time sandbox blocks something, it looks like this:
Error: EPERM: operation not permitted, open '/Users/you/.cache/mise/...'
Or sometimes just a cryptic failure from a tool that expected to write somewhere and couldn't. Every tool with a cache directory outside your project directory fails. mise, Bundler, npm, yarn, cargo, pip. They all write to ~/ somewhere, and the sandbox says no.
The fix is filesystem.allowWrite. You add each path the sandbox needs to allow, organized by what ecosystem needs it. This is the tedious part, but it's front-loaded. Once you've covered your stack, you rarely hit new ones.
Here's what the Ruby, Node, and Go ecosystems need:
Ruby:
"~/.bundle",
"~/.gem",
"~/.rbenv",
"~/.cache/bundler",
"~/.cache/rubygems",
"~/Library/Caches/bundle"
Node (npm, yarn, pnpm, corepack):
"~/.npm",
"~/.config/npm",
"~/.cache/npm",
"~/.cache/node",
"~/.node-gyp",
"~/.cache/node-gyp",
"~/.config/configstore",
"~/Library/Caches/npm",
"~/.pnpm-store",
"~/.pnpm-state",
"~/.config/pnpm",
"~/.local/share/pnpm",
"~/.local/state/pnpm",
"~/Library/Caches/pnpm",
"~/Library/Preferences/pnpm",
"~/Library/pnpm",
"~/.yarn",
"~/.yarnrc",
"~/.yarnrc.yml",
"~/.config/yarn",
"~/.cache/yarn",
"~/Library/Caches/Yarn",
"~/.cache/node/corepack",
"~/Library/Caches/node/corepack"
Go:
"~/.cache/go-build",
"~/.config/go",
"~/.local/share/go",
"~/go"
Notice the pattern: most tools have paths in ~/.cache/, ~/.config/, or ~/.local/. macOS tools also often use ~/Library/Caches/. You need both. I pulled most of these from agent-safehouse, a community-maintained set of macOS seatbelt profiles for AI agents. Good reference if you're building your own list.
Beyond the ecosystem caches, there are a few system paths you'll probably need:
PTY allocation (lefthook, interactive tools):
"/dev/ptmx",
"/dev/ttys*"
Claude plugin cache (hooks use uv, need .venv creation):
"~/.claude/plugins/cache"
mise version manager:
"~/.cache/mise",
"~/.local/share/mise",
"~/.config/mise",
"~/.local/state/mise",
"~/Library/Caches/mise"
Misc: "~/.task" (Taskwarrior)
One gotcha: the sandbox maps $TMPDIR to a writable temp directory, but /tmp may not be directly writable. If a tool writes to /tmp/something and fails, use $TMPDIR instead. This catches people.
You can steal from other people's configs to shortcut the discovery process. But you'll still hit new paths occasionally when you adopt a new tool. "Oh, that needs a cache directory? Let me add that." Manageable.
Unix Sockets
Things that talk over Unix domain sockets stop working in sandbox. GPG signing on git commits, Docker and Colima, git's core.fsmonitor file watcher. All dead.
"network": {
"allowAllUnixSockets": true
}
This allows all Unix sockets, not specific ones. The granular version (allowUnixSockets with per-path entries) requires knowing every socket path your tools use, and those paths change across tool versions. In practice, allowAllUnixSockets is the right call. Unix sockets are local-only communication, not network egress. Not a real concern.
Network: TLS, Local Binding, and Allowed Hosts
Three network settings, each solving a different problem.
Go TLS
"enableWeakerNetworkIsolation": true
Go-based tools (gh, bk, gcloud, terraform) use their own TLS implementation. They need access to macOS's trust daemon (com.apple.trustd.agent) for certificate verification. The sandbox blocks this by default. The setting is called "weaker" because it opens a path to the trust service. Technically a data exfiltration vector. In practice, you need it for any Go CLI to verify TLS certificates.
Local Binding
"network": {
"allowLocalBinding": true
}
Some tools unexpectedly start local servers. Pre-commit hooks that spin up a linter server, clippy --fix in Rust (for some reason??), OAuth callback flows that run a temporary web server to receive the redirect. Without allowLocalBinding, these fail silently or with confusing errors.
Allowed Hosts
"network": {
"allowedHosts": [
"registry.npmjs.org",
"index.crates.io",
"static.crates.io",
"api.github.com",
"github.com",
"api.buildkite.com",
"code.claude.com",
"docs.github.com",
"mise.jdx.dev",
"mise-versions.jdx.dev"
]
}
The network egress allowlist. Every host your tools need to reach goes here. Your list will look different from mine. Package registries, APIs, documentation sites, whatever your workflow touches.
Same iterative process as filesystem paths: hit a network error in sandbox, check what host it was trying to reach, add it. You build this up over the first few days and then it stabilizes.
What's Still Rough
Sandbox is worth the setup. It's not frictionless though. Here's what I'm still sorting out.
Git Hooks and Protected Paths
This is the biggest one for me. Claude treats .git/hooks, .gitconfig, .vscode/, and .idea/ as protected paths. This is Claude's choice, not the OS sandbox. Important distinction. The seatbelt profile isn't blocking these, Claude is.
If the agent manages worktrees or touches hook directories, those operations need to run outside the sandbox. Claude's built-in worktree mode handles some of this, but if you use the agent for repository management like I do, you'll hit this constantly.
The Model Preemptively Unsandboxing
Sometimes Claude decides to set dangerouslyDisableSandbox: true before even trying the command in the sandbox. No prior failure, no "operation not permitted" error. Just a preemptive decision to skip sandbox entirely. The system prompt tells the model to try sandboxed first and only disable after evidence of sandbox restrictions, but it doesn't always listen. GitHub issue #34315 has the best analysis of this behavior and some workarounds.
Bash Parser Prompts
Some command shapes trigger permission prompts regardless of sandbox status because of the Bash tool's AST analysis. That's a separate system from sandbox. If autoAllowBashIfSandboxed isn't suppressing prompts for a particular command, this may be why.
What Sandbox Doesn't Cover
Sandbox only applies to Bash tool calls. The Read, Edit, and Write tools go through Claude's permission system, not the OS sandbox.
What this means in practice: if you configure the sandbox to deny writes to ~/.ssh, Claude can't cat ~/.ssh/id_rsa through Bash. But the Read tool could still read that file. The OS-level isolation only wraps Bash.
Doesn't make sandbox worthless. Bash is the highest-risk tool because it can execute arbitrary code, make network requests, and write anywhere on disk. Sandboxing Bash covers the biggest attack surface. But "sandbox enabled" doesn't mean "fully isolated."
There's also denyWrite and denyRead for explicitly protecting sensitive paths like ~/.ssh or ~/.aws/credentials. Worth knowing about.
The broader isolation picture (sandbox vs. external containers vs. dev containers) is a whole other post. This one is about making the built-in sandbox work for daily dev.
Full Config Reference
Here's the complete sandbox block I'm running. Copy, adapt, iterate.
{
"sandbox": {
"enabled": true,
"autoAllowBashIfSandboxed": true,
"enableWeakerNetworkIsolation": true,
"network": {
"allowAllUnixSockets": true,
"allowLocalBinding": true,
"allowedHosts": [
"api.anthropic.com",
"code.claude.com",
"api.github.com",
"docs.github.com",
"github.com",
"raw.githubusercontent.com",
"registry.npmjs.org",
"index.crates.io",
"static.crates.io",
"static.rust-lang.org",
"crates.io",
"docs.rs",
"formulae.brew.sh",
"api.buildkite.com",
"buildkite.com",
"mise.jdx.dev",
"mise-versions.jdx.dev",
"hk.jdx.dev"
]
},
"filesystem": {
"allowWrite": [
"/dev/ptmx",
"/dev/ttys*",
"~/.claude/plugins/cache",
"~/.bundle",
"~/.gem",
"~/.rbenv",
"~/.cache/bundler",
"~/.cache/rubygems",
"~/Library/Caches/bundle",
"~/.npm",
"~/.node-gyp",
"~/.cache/node-gyp",
"~/.cache/npm",
"~/.cache/node",
"~/.cache/yarn",
"~/.cache/node/corepack",
"~/.config/npm",
"~/.config/configstore",
"~/.config/yarn",
"~/.config/pnpm",
"~/.pnpm-state",
"~/.pnpm-store",
"~/.yarn",
"~/.yarnrc",
"~/.yarnrc.yml",
"~/.local/share/pnpm",
"~/.local/state/pnpm",
"~/Library/Caches/npm",
"~/Library/Caches/Yarn",
"~/Library/Caches/node/corepack",
"~/Library/Caches/pnpm",
"~/Library/Preferences/pnpm",
"~/Library/pnpm",
"~/Library/Caches/ms-playwright",
"~/Library/Caches/Cypress",
"~/.cache/puppeteer",
"~/.cache/pip",
"~/.cache/uv",
"~/.config/uv",
"~/.local/share/uv",
"~/.local/state/uv",
"~/Library/Caches/pip",
"~/Library/Caches/uv",
"~/.cache/go-build",
"~/.config/go",
"~/.local/share/go",
"~/go",
"~/.cache/golangci-lint",
"~/Library/Caches/go-build",
"~/Library/Caches/golangci-lint",
"~/.cargo",
"~/.rustup",
"~/Library/Caches/cargo",
"~/.cache/mise",
"~/.config/mise",
"~/.local/share/mise",
"~/.local/state/mise",
"~/Library/Caches/mise",
"~/.docker",
"~/.colima",
"~/.config/gh",
"~/.cache/gh",
"~/.local/share/gh",
"~/.local/state/gh",
"~/.cache/pre-commit",
"~/.cache/nvim/",
"~/.task",
"~/Library/Caches/dotslash"
]
}
}
}
Adjust to your stack. If you don't use Rust, drop those lines. If you use Java, you'll need ~/.gradle and friends. The pattern is always the same: cache directory, config directory, maybe a ~/Library/Caches/ counterpart on macOS.
Organizing Your Config
Once your sandbox block has 50+ allowWrite paths and 20+ allowedHosts, the flat list gets hard to reason about. You start wondering "why is this path here?" and "which tool needs this host?"
One approach I've landed on: break the config into per-ecosystem files and merge them. A base role holds the sandbox scalars (enabled, autoAllowBashIfSandboxed, etc.) and system paths. Then each stack file co-locates the sandbox paths with the permissions for that tool, so everything about the Ruby ecosystem lives in one place:
{
"permissions": {
"allow": [
"Bash(bundle:*)",
"Bash(gem:*)",
"Bash(ruby:*)"
]
},
"sandbox": {
"filesystem": {
"allowWrite": [
"~/.bundle",
"~/.gem",
"~/.rbenv",
"~/.cache/bundler",
"~/.cache/rubygems",
"~/Library/Caches/bundle"
]
}
}
}
A merge script concatenates all the files and deduplicates the arrays into the final settings.json.
You don't have to do this. The flat settings.json works fine, and comments help. But if you're managing configs across machines or sharing sandbox setups with a team, it pays for itself. My dotfiles repo has the full implementation if you want to see how it fits together.
The Deal
Two weeks ago I expected to bail on this within the hour. Instead I've got a config that lets the agent run most commands without asking, actual OS-level isolation on the ones that matter, and a clear picture of where the boundaries are.
The setup isn't zero-effort. You'll spend your first few sessions adding cache paths and hosts as you hit them. But that stabilizes fast, and what you get back is a much quieter workflow with real guardrails instead of permission fatigue.
Sandbox covers Bash. It doesn't cover everything. For the full isolation picture, I'm working on a follow-up comparing sandbox, external containers, and dev containers. But for daily dev work with Claude Code, this config is the one that actually works.