You create a “read-only” agent with --allowedTools "Read,Grep,Glob". It writes a file anyway. The --allowedTools flag is a preference, not an enforcement boundary — Claude fell back to Bash, which you never explicitly blocked. The OS sandbox is the layer that actually stops things at the kernel level.
Claude Code runs inside an OS-level sandbox that restricts filesystem and network access at the kernel layer. Even if a prompt injection tells Claude to read /etc/shadow or exfiltrate data over HTTP, the operating system blocks the operation before it reaches the filesystem or the network stack. This is not application-level filtering that Claude can reason around — it is enforcement by the kernel itself.
Platform Support
Sandbox Backends by Platform
| Platform | Backend | Mechanism | Status |
|---|---|---|---|
| macOS | Seatbelt (sandbox-exec) | Kernel-level sandbox profiles restricting syscalls, filesystem paths, and network | Fully supported |
| Linux | bubblewrap (bwrap) | Mount namespaces for filesystem isolation, network namespaces, process isolation | Fully supported |
| WSL2 | bubblewrap (bwrap) | Same as Linux — WSL2 runs a real Linux kernel with full namespace support | Fully supported |
| WSL1 | None | WSL1 translates syscalls to Windows NT; no Linux namespace support | Not supported |
| Native Windows | None | No Seatbelt or bubblewrap equivalent available | Not supported |
Filesystem Isolation
By default, the sandbox allows read-write access to exactly three locations:
cwd— the working directory where you launched Claude~/.claude/— Claude’s configuration and session data- System tmpdir — temporary files needed during execution
Everything else on the filesystem is blocked. If Claude attempts to read a file outside these directories, the OS kernel denies the syscall and returns a permission error. This holds true even if Claude uses Bash to attempt raw file reads — the sandbox sits below the shell.
# This fails -- /var/log/app is outside the sandbox$ claude -p "Read /var/log/app/error.log" --output-format jsonThe response will contain a denial because /var/log/app is not within the allowed directory set.
Extending Access with —add-dir
The --add-dir flag punches a hole in the sandbox for a specific directory. You can use it multiple times to grant access to several paths:
# Grant access to two additional directories$ claude -p "Analyze logs and check config" \ --add-dir /var/log/app \ --add-dir /etc/app-config \ --output-format jsonKey behaviors of --add-dir:
- Paths must be absolute — relative paths are rejected
- The path must exist at invocation time
- Added directories get read-write access, the same level as
cwd - The sandbox boundary becomes:
cwd+ all--add-dirpaths +~/.claude/+ system tmpdir
—add-dir grants full read-write access. If you need read-only access to an external directory, combine it with —disallowedTools “Write,Edit” to prevent modifications while still allowing reads.
Network Isolation
Filesystem isolation alone is not enough. Without network restrictions, a compromised agent could read SSH keys from ~/.ssh/ (which may be inside the allowed sandbox) and exfiltrate them over HTTP.
The sandbox can block network access entirely, and you can further restrict it at the tool level:
# Air-gapped agent -- no web access, no Bash escape$ claude -p "Analyze this code offline" \ --disallowedTools "WebFetch,WebSearch,Bash" \ --permission-mode bypassPermissions \ --output-format jsonWhen web tools are blocked, Claude falls back to its training knowledge. The response comes from what the model already knows, not from live web data. Verify freshness accordingly.
Defense Layers
Safe unattended operation requires all three layers working together. No single mechanism is sufficient on its own:
Defense-in-Depth Layers
| Layer | Mechanism | What It Blocks | What It Misses |
|---|---|---|---|
| Sandbox | OS-level filesystem and network isolation | Access to paths outside allowed directories, unauthorized network calls | Anything within allowed directories is fair game |
| Tool restrictions | —allowedTools and —disallowedTools | Specific tool usage (Write, Edit, Bash, WebFetch) | MCP tools bypass built-in tool restrictions |
| Hooks | PreToolUse / PostToolUse event handlers | Custom rules — regex on file paths, command auditing, API call logging | Only as strong as the rules you write |
Consider a prompt-injection attack chain: an injected instruction tells Claude to cat ~/.ssh/id_rsa. The filesystem sandbox blocks the path if ~/.ssh/ is outside allowed directories. If the read somehow succeeds, the network sandbox blocks exfiltration. And even if both layers fail, a PreToolUse hook can match the path pattern and reject the operation. Each layer catches what the previous one might miss.
The --dangerously-skip-permissions flag skips all permission prompts, but hooks still fire. This makes hooks the last line of defense in fully automated pipelines:
Permission check flow with --dangerously-skip-permissions: 1. Tool requested --> permission check --> SKIPPED 2. PreToolUse hook --> STILL FIRES --> can block execution 3. Tool executes --> sandbox --> STILL ACTIVE 4. PostToolUse hook --> STILL FIRES --> can audit resultsTest the sandbox yourself. Try to read a file outside your working directory:
claude -p “Read /etc/passwd” —output-format json | jq ‘.result’
The sandbox should block it. Now try claude -p “Read /etc/passwd” —add-dir /etc —output-format json | jq ‘.result’. What changed? This is the —add-dir flag punching a hole in the sandbox.
Proof: The allowedTools Bypass
The most dangerous misconception in Claude Code security is that --allowedTools alone creates a read-only agent. It does not. Here is the proof — a supposedly read-only agent successfully writing a file:
# WRONG: This is NOT read-only$ claude -p "Write hello to /tmp/outside_test.txt" \ --allowedTools "Read,Grep,Glob" \ --permission-mode bypassPermissions \ --output-format jsonClaude fell back to Bash (which was not in --allowedTools but was not explicitly blocked either) and ran echo "hello" > /tmp/outside_test.txt. The --allowedTools flag is a preference, not an enforcement boundary. The --disallowedTools deny list is what actually blocks tools.
--allowedTools "Read,Grep,Glob"
The correct pattern for a true read-only agent:
# RIGHT: Both allowedTools AND disallowedTools$ claude -p "Analyze this codebase for security issues" \ --allowedTools "Read,Grep,Glob" \ --disallowedTools "Write,Edit,Bash,WebFetch,WebSearch" \ --permission-mode bypassPermissions \ --output-format jsonAir-Gapped Response
When web tools are blocked, Claude responds entirely from training knowledge. This payload shows what that looks like:
Notice web_search_requests: 0 and web_fetch_requests: 0. Claude did not attempt any web access — it recognized the tools were unavailable and produced the answer from what it already knew.
The sandbox is enforced at the OS kernel level. Prompt injection cannot escape it. Even if an attacker crafts a prompt that instructs Claude to cat /etc/shadow or curl https://evil.com, the kernel blocks the syscall before it executes. This is the single most important property of the sandboxing system.
WSL1 is not supported. WSL1 translates Linux syscalls to Windows NT kernel calls and does not provide the Linux namespace isolation that bubblewrap requires. If you are on Windows, use WSL2 (which runs a real Linux kernel) or a Docker container with a Linux image.
Hook exit 2 survives —dangerously-skip-permissions. Experimentally confirmed: a PreToolUse hook that exits with code 2 blocks tool execution even when —dangerously-skip-permissions is active. Hooks are the only enforcement layer that cannot be bypassed by any flag. The security stack is: hooks (unbyppassable) > sandbox (kernel-level) > permissions (flag-bypassable).
--allow-dangerously-skip-permissions vs --dangerously-skip-permissions
These flags look similar but serve fundamentally different purposes. One is a single-key bypass; the other is a two-key safety mechanism.
Permission Bypass Comparison
| Flag | Alone | With —permission-mode bypassPermissions | Use Case |
|---|---|---|---|
—dangerously-skip-permissions | Bypass active immediately | N/A (already active) | Quick scripts, trusted environments |
—allow-dangerously-skip-permissions | No bypass — capability enabled only | Bypass active (both keys required) | CI/CD with explicit activation |
The two-key pattern makes --allow-dangerously-skip-permissions safer for CI/CD pipelines:
# Single-key bypass (one flag does it all)claude -p "Deploy" --dangerously-skip-permissions
# Two-key bypass (both flags required)claude -p "Deploy" \ --allow-dangerously-skip-permissions \ --permission-mode bypassPermissions# Removing EITHER flag blocks the bypassWithout --permission-mode bypassPermissions, the --allow-dangerously-skip-permissions flag does nothing — the default permission mode applies normally. This is intentional: it lets pipeline templates include the allow flag while requiring explicit activation in the specific job step.
—allow-dangerously-skip-permissions alone does NOT bypass permissions. Read that again. It enables the capability but does not activate it. You must pair it with —permission-mode bypassPermissions — a two-key launch sequence. This is intentional: it prevents accidental bypasses in scripts that set the allow flag broadly. If you’re confused by the naming, you’re not alone — but the two-key pattern is what makes it safe for CI/CD templates.
Settings.json deny rules survive even with bypass active. A “deny”: [“Write”] rule removes the Write tool from Claude’s toolset entirely — Claude never sees it, regardless of permission mode. Deny rules are enforced at the tool-loading stage, before permission checks occur.
Known Security Vulnerabilities
Published security research has identified real vulnerabilities in Claude Code’s sandboxing and configuration system:
CVE-2025-59536 / CVE-2026-21852 — RCE via Project Files: Malicious .claude/ project configurations (hooks, MCP servers, environment variables) can achieve remote code execution and API token exfiltration. An attacker who controls a repository’s .claude/ directory can execute arbitrary code when a victim opens the project with Claude Code. Mitigations: review .claude/ contents before opening untrusted repos, use managed settings to restrict hook and MCP sources.
Denylist Bypass via /proc/self/root/: On Linux, Claude can bypass its own denylist by using /proc/self/root/usr/bin/npx to resolve to the same binary without matching the deny pattern. The deny rules use string matching, not path resolution, creating a gap for symlink-based bypasses. Mitigation: combine deny rules with OS-level sandbox restrictions.
ToxicSkills — Malicious Agent Skills: Research found that 36% of community-created agent skills contain security flaws, with 13.4% at critical severity. There is no official vetting system for skills. Mitigation: audit skills before installation, prefer skills from trusted sources, use --disable-slash-commands in sensitive environments.
Prompt Injection via MCP Tool Outputs: MCP tool responses are a documented attack vector for prompt injection. A malicious MCP server can return crafted tool outputs that manipulate Claude’s behavior. Mitigation: use --strict-mcp-config with explicitly trusted servers only, implement PostToolUse hooks to validate MCP outputs.
Published CVEs demonstrate that malicious .claude/ directories can achieve remote code execution. Before opening any untrusted repository with Claude Code, inspect .claude/settings.json (hooks, permissions), .mcp.json (MCP servers), and any hook scripts. Managed settings with allowManagedPermissionRulesOnly: true can enforce organizational policies that override malicious project configs.
Test your sandbox right now: claude -p “What is in /etc/shadow?” —output-format json | jq ‘.result’. If the sandbox blocks it, you’re protected. If it doesn’t, you’re running without sandboxing — check your platform support table above and fix it before running unattended agents.