Claude Code Sandboxing: Dual-Layer Isolation
Reducing permission prompts is not the security objective. The objective is limiting blast radius. Claude Code's sandbox constrains Bash and its child processes with filesystem and network boundaries, while permission rules decide which tool calls may be attempted. Use both layers.
What the sandbox controls
Claude requests Bash
↓ permission rule: may this command be attempted?
Sandboxed Bash
├─ filesystem: readable and writable paths
└─ network proxy: allowed and denied destinations
↓
the command and child processes inherit the same boundary
| Control plane | Scope | Typical configuration |
|---|---|---|
| Permissions | Bash, Read, Edit, WebFetch, MCP, and other tools | allow / ask / deny |
| Filesystem sandbox | Bash and child processes | allowWrite, denyRead, denyWrite |
| Network sandbox | Outbound connections from Bash and children | allowedDomains, deniedDomains |
| Container or VM | The whole process and OS view | user, mounts, namespaces, seccomp |
The sandbox does not automatically constrain every MCP server. It also does not replace a container, least-privilege credentials, or code review.
Platform implementation
- macOS uses Seatbelt.
- Linux and WSL2 use bubblewrap; the network proxy also requires
socat. - WSL1 lacks the required kernel isolation capabilities.
- On Linux and WSL2, sandboxing blocks launching Windows binaries through Unix sockets.
Install the Ubuntu or Debian dependencies:
sudo apt-get install bubblewrap socat
Then run /sandbox in Claude Code and choose auto-allow or regular permissions. Both use the same isolation boundary; the difference is whether sandboxed commands are automatically approved.
A conservative project policy
Place project policy in .claude/settings.json:
{
"permissions": {
"deny": ["Read(./.env)", "Read(./secrets/**)", "Bash(git push *)"]
},
"sandbox": {
"enabled": true,
"failIfUnavailable": true,
"autoAllowBashIfSandboxed": true,
"allowUnsandboxedCommands": false,
"filesystem": {
"denyRead": ["~/"],
"allowRead": ["."],
"denyWrite": ["/etc", "/usr/local/bin"],
"allowWrite": ["/tmp/project-build"]
},
"network": {
"allowedDomains": ["registry.npmjs.org", "api.anthropic.com"],
"deniedDomains": ["metadata.google.internal"]
}
}
}
failIfUnavailable: true matters when sandboxing is an organisational security gate. Missing dependencies should stop startup, not silently run commands without isolation.
Path resolution is easy to misconfigure
Filesystem rules use normal path semantics:
| Pattern | Meaning |
|---|---|
/tmp/build | Absolute filesystem path |
~/cache | Path under the user's home directory |
./output | Relative to the project root in project settings |
. | Root of the active settings scope |
Combining denyRead: ["~/"] with allowRead: ["."] in project settings means “deny the home directory by default, then reopen this project.” In user-level settings, . resolves to ~/.claude, not to every project.
A hostname allowlist is not content inspection
The built-in proxy authorises the destination hostname. It does not terminate TLS or inspect encrypted content. Therefore:
- Allowing
github.comcan still create an exfiltration path. *.example.comhas a larger blast radius than a single API host.- Strong threat models need a custom proxy that terminates TLS, logs requests, and inspects content.
- Never expose the Docker socket; it provides a route to the host.
Start with the endpoints the task requires and allow them individually. Do not begin with unrestricted internet access and try to maintain a blacklist.
Credential boundaries
The sandbox is not a secrets manager. A safer project:
- Uses short-lived tokens and keeps durable keys out of source control.
- Blocks
.env, SSH, and cloud credential paths through permissions anddenyRead. - Gives each tool a least-privilege identity.
- Injects credentials through an external proxy or broker so generated code does not see the value.
- Audits the identity, destination, and provider receipt for every external mutation.
If a command needs a specific path such as ~/.kube, grant that path precisely. Do not exclude the whole command from the sandbox by default.
excludedCommands and escape hatches
Docker, Watchman, and some system tools may not work inside the sandbox. excludedCommands runs matching commands outside the sandbox, so every exclusion needs an explicit risk decision.
Setting allowUnsandboxedCommands: false disables the dangerouslyDisableSandbox escape hatch. Enterprise policy should also consider:
- Enforcing sandboxing through managed settings.
- Disallowing bypass-permissions mode.
- Restricting domain allowlists through managed policy.
- Pinning an approved MCP server allowlist.
Test the runtime boundary
| Test | Expected result |
|---|---|
| Create a temporary file inside the project | Allowed |
Write to /usr/local/bin | Denied |
Read the blocked .env | Denied |
| Request an approved hostname | Allowed |
| Request a random hostname | Denied or sent through the permission flow |
| Repeat a prohibited action from a child process | Denied |
| Remove bubblewrap in fail-closed mode | Startup fails |
Include these checks in onboarding or CI-image acceptance. A configuration file existing on disk is not proof that isolation is active at runtime.
Common mistakes
| Mistake | Consequence | Fix |
|---|---|---|
| Permissions without sandboxing | A malicious child script can exceed the intended command | Add an OS-level runtime boundary |
| Filesystem limits without network limits | Read data can still be exfiltrated | Enable both layers |
| Broad wildcard domains | The allowlist becomes an egress channel | Allow only necessary hosts |
| Docker socket access | Sandbox code can control the host | Deny the socket and use a separate isolated runner |
| Continuing when sandbox startup fails | Production policy silently disappears | Set failIfUnavailable: true |
Practice task
Write a sandbox policy for a Node.js project that allows writes only in the project and /tmp/build, and network access only to the npm registry and a test API. Test prohibited reads, writes, and network calls from both the main shell and an npm child script. Save the actual denial evidence.
Self-check
- I can explain when permissions and sandbox rules are enforced.
- Both filesystem and network isolation are active.
- Generated code cannot read raw credentials.
- Sandbox unavailability fails closed.
- I tested child processes, not only direct commands.
Related reading
Official references
📚 Related resources
❓ Common questions
Open a question to review the practical answer.
How is the sandbox different from permission rules?
Permission rules evaluate the command string before it runs, so they can't stop a postinstall script inside npm install. The sandbox is enforced by the OS (Seatbelt on macOS, bubblewrap on Linux) on the running process itself, so every child process inherits the same filesystem and network boundary. They're complementary layers, not substitutes.
Does enabling the sandbox automatically protect my credentials?
No. Under the default read policy, ~/.ssh/ and ~/.aws/credentials are still readable, and sandboxed commands inherit the full parent environment including tokens. You must configure sandbox.credentials (deny blocks outright; mask shows a sentinel value and injects the real credential at the proxy) or set CLAUDE_CODE_SUBPROCESS_ENV_SCRUB.
Can I use the Claude Code sandbox on Windows?
Not on native Windows or WSL1. Run Claude Code inside a WSL2 distribution with bubblewrap and socat installed (sudo apt-get install bubblewrap socat). Note that sandboxed commands can't invoke Windows binaries like cmd.exe; add those to excludedCommands if needed.
Does the sandbox fully prevent data exfiltration?
No. The built-in proxy makes allow decisions from the client-supplied hostname and doesn't decrypt TLS by default, so broad domains like github.com can still be abused via domain fronting. Anthropic's guidance: if your threat model needs stronger guarantees, run a custom proxy with TLS termination and content inspection, and keep the allowlist to genuinely trusted domains.