Version: March 2026 | Applies to: Claude Code v2.1+
Claude Code’s security features evolve rapidly. Verify settings against official docs if this guide is more than 3 months old.
1. Overview
By default, Claude Code:
- Can read files anywhere on the system (useful for system libraries and dependencies)
- Can write only to the working directory (CWD) and its subdirectories
- Cannot write to parent directories without explicit permission
Without sandbox enabled, this write restriction is Claude’s own policy, NOT OS-enforced. Nothing physically prevents a Bash command from writing outside the project unless the sandbox is turned on. With sandbox enabled ("sandbox": {"enabled": true}), the OS enforces the restriction — Bash commands are physically blocked from writing outside CWD.
WARNING: CLAUDE.md Is NOT a Security Control
Instructions in
CLAUDE.mdare loaded as context for the model. They are suggestions Claude tries to follow, not enforced restrictions. Writing “never access files outside this directory” in CLAUDE.md provides zero security guarantee. For enforceable restrictions, use permission rules, sandbox, and hooks as described below.
2. Threat Model
Before configuring controls, understand what you’re protecting against:
| Threat | Control | Residual Risk |
|---|---|---|
Reads secret files (.env, credentials) | Sandbox denyRead + permission deny rules + .claudeignore | Bash cat ~/.env bypasses Read-tool rules without sandbox |
| Writes outside project directory | Sandbox restricts Bash writes to CWD only (OS-enforced) + hooks | None if sandbox enabled |
| Exfiltrates data via network | Sandbox allowedDomains | HTTPS content not inspected; domain fronting possible |
| Modifies system configs | Sandbox denyWrite to system paths | Symlink-based bypass if paths not canonicalized |
| Env variable leakage via Bash | Permission deny rules for Bash commands | Pattern matching is fragile; echo $AWS_KEY has many variants |
| Deletes critical files | Sandbox write restrictions + hooks on rm commands | Allowed paths are still deletable |
3. Quick Start: Recommended Enterprise Configuration
Get secure first, understand why later. Place this in .claude/settings.json in your project:
{
"permissions": {
"defaultMode": "default",
"deny": [
"Read(./.env)",
"Read(~/.aws/credentials)",
"Read(~/.ssh/*)",
"Bash(rm -rf *)",
"Bash(rm -r *)"
]
},
"sandbox": {
"enabled": true,
"autoAllowBashIfSandboxed": true,
"allowUnsandboxedCommands": false,
"filesystem": {
"denyRead": ["~/.aws/credentials", "~/.ssh"],
"denyWrite": ["//etc", "//usr", "~/.bashrc", "~/.zshrc"]
},
"network": {
"allowedDomains": ["github.com", "*.npmjs.org", "registry.yarnpkg.com"]
}
},
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/validate-file-path.sh"
}
]
}
]
}
}
Create the hook script at .claude/hooks/validate-file-path.sh:
#!/bin/bash
# Block writes outside the project directory
# Hook receives JSON on stdin with tool name and arguments
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.command // ""')
# Get the project root (where .claude/ lives)
PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
# Resolve the target path (handles both absolute and relative paths)
if [ -n "$FILE_PATH" ]; then
# Convert relative paths to absolute
if [[ "$FILE_PATH" != /* ]]; then
FILE_PATH="$PROJECT_ROOT/$FILE_PATH"
fi
# Resolve symlinks and .. components
RESOLVED=$(cd "$(dirname "$FILE_PATH")" 2>/dev/null && pwd)/$(basename "$FILE_PATH") || RESOLVED="$FILE_PATH"
if [[ "$RESOLVED" != "$PROJECT_ROOT"* ]]; then
echo "BLOCKED: Write to $FILE_PATH is outside project directory $PROJECT_ROOT" >&2
exit 2 # Exit code 2 = block the action
fi
fi
exit 0 # Allow
Make it executable:
chmod +x .claude/hooks/validate-file-path.sh
Also create a .claudeignore file in the project root:
# Secrets
.env
.env.*
*.pem
*.key
# Credentials
.aws/
.ssh/
# Large/irrelevant directories
node_modules/
.git/objects/
4. .claudeignore — The Simplest Restriction
.claudeignore uses gitignore syntax and prevents Claude Code from reading or indexing matched files. It’s the easiest first line of defense.
Location: Project root (next to .claude/ directory)
Example .claudeignore:
# Secrets and credentials
.env
.env.local
.env.production
*.pem
*.key
*.p12
# Cloud credentials
.aws/
.gcp/
.azure/
# SSH keys
.ssh/
# Build artifacts
dist/
build/
node_modules/
What it does:
- Claude’s
Readtool will refuse to read matched files - Files are excluded from codebase indexing and search
- Applies to the project scope only
What it does NOT do:
- Does not prevent
Bash(cat .env)— Bash can still read these files - Does not apply to MCP server file access
- Does not restrict write access
Best used with: Sandbox denyRead rules for defense-in-depth.
5. Permission Modes
Set the overall permission behavior for the session:
| Mode | Read files | Edit files | Run Bash | Prompt behavior |
|---|---|---|---|---|
default | Auto-allowed | Prompts | Prompts | Standard interactive |
acceptEdits | Auto-allowed | Auto-allowed | Prompts | Good for trusted editing |
plan | Auto-allowed | Blocked | Blocked | Read-only analysis |
dontAsk | Auto-allowed | Per rules only | Per rules only | Auto-denies unless pre-approved via /permissions or allow rules |
bypassPermissions | Auto-allowed | Auto-allowed | Auto-allowed | Dangerous: no prompts at all |
How to set:
# CLI flag
claude --permission-mode plan
# In .claude/settings.json
{
"permissions": {
"defaultMode": "dontAsk"
}
}
# During session
/permissions
Precedence: deny rules always win, regardless of permission mode. If you deny Read(./.env) and use bypassPermissions, the .env read is still blocked.
Enterprise recommendation: Use default or dontAsk mode. Never allow bypassPermissions — disable it with:
{
"permissions": {
"disableBypassPermissionsMode": "disable"
}
}
Note:
disableBypassPermissionsModeis a managed-settings-only field (requires Teams/Enterprise).
6. Permission Rules (allow / deny / ask)
Fine-grained control over which tools can access which paths.
Syntax
{
"permissions": {
"allow": ["tool(pattern)"],
"ask": ["tool(pattern)"],
"deny": ["tool(pattern)"]
}
}
Path Prefixes
| Prefix | Meaning | Example |
|---|---|---|
// | Absolute filesystem path | Read(//etc/passwd) |
~/ | Home directory | Read(~/.aws/credentials) |
/ | Relative to project root | Edit(/src/**/*.ts) |
./ or none | Relative to CWD | Read(./.env) |
Examples
{
"permissions": {
"allow": [
"Bash(npm run *)",
"Bash(git status)",
"Bash(git diff *)",
"Read(/src/**)",
"Edit(/src/**/*.ts)"
],
"deny": [
"Read(./.env)",
"Read(~/.aws/credentials)",
"Read(~/.ssh/*)",
"Bash(curl *)",
"Bash(wget *)",
"Bash(rm -rf *)",
"Edit(//etc/*)",
"Write(//usr/*)"
]
}
}
Settings File Locations & Precedence
From highest to lowest priority:
- Managed settings (Teams/Enterprise admin) — cannot be overridden
- CLI arguments (
--permission-mode,--allowedTools) .claude/settings.local.json— local project, not committed.claude/settings.json— shared project settings~/.claude/settings.json— user-level defaults
Important Notes
- Symlinks: Permission rules match on the path as given, not the resolved target. A symlink
./safe-link -> /etc/passwdmay bypass a deny rule on//etc/passwdif accessed viaRead(./safe-link). - Bash pattern fragility:
Bash(curl *)blockscurl http://example.combut NOTcommand curl,/usr/bin/curl, orcurlvia a shell script. For network control, use sandboxallowedDomainsinstead. denyalways wins: A path in bothallowanddenyis denied.
7. Sandbox (OS-Level Enforcement)
The sandbox provides OS-enforced restrictions on Bash commands, unlike permission rules which are policy-based.
When sandbox is enabled, Bash can only write to CWD and subdirectories by default. This is enforced by the operating system (macOS Seatbelt / Linux bubblewrap), not by Claude’s policy. A sandboxed echo "test" > /tmp/file.txt will fail at the OS level.
How It Works
| OS | Technology | Status |
|---|---|---|
| macOS | Seatbelt (sandbox-exec) | Supported |
| Linux / WSL2 | bubblewrap (bwrap) | Supported |
| WSL1 | — | Not supported |
| Windows (native) | — | Not yet available |
Configuration
{
"sandbox": {
"enabled": true,
"autoAllowBashIfSandboxed": true,
"allowUnsandboxedCommands": false,
"filesystem": {
"allowWrite": [],
"denyWrite": ["//etc", "//usr", "~/.bashrc", "~/.zshrc"],
"denyRead": ["~/.aws/credentials", "~/.ssh"]
},
"network": {
"allowedDomains": ["github.com", "*.npmjs.org"],
"allowUnixSockets": [],
"allowManagedDomainsOnly": false
}
}
}
Key Settings
| Setting | Purpose |
|---|---|
sandbox.enabled | Enable OS-level sandboxing for Bash |
allowUnsandboxedCommands | Set false to block the dangerouslyDisableSandbox escape hatch |
filesystem.allowWrite | Additional directories Bash can write to (beyond CWD) |
filesystem.denyWrite | Directories Bash cannot write to, even within allowed scope |
filesystem.denyRead | Directories Bash cannot read from |
network.allowedDomains | Whitelist of domains Bash commands can reach |
autoAllowBashIfSandboxed | Skip Bash approval prompts when sandbox is active |
Note on path syntax: Sandbox filesystem paths (e.g., in
denyRead,denyWrite) use~/for home directory and//for absolute paths, matching the permission rule syntax. Verify the exact syntax against your Claude Code version, as this may evolve.
dangerouslyDisableSandbox
The Bash tool has a dangerouslyDisableSandbox parameter that runs commands outside the sandbox. To completely prevent this:
{
"sandbox": {
"allowUnsandboxedCommands": false
}
}
With this set, any Bash call requesting dangerouslyDisableSandbox: true will be denied outright.
Sandbox Unavailability
If the sandbox runtime is not available (e.g., bubblewrap not installed on Linux), behavior depends on configuration:
- With
allowUnsandboxedCommands: true(default): falls back to permission-based approval - With
allowUnsandboxedCommands: false: Bash commands are blocked entirely
8. Hooks (Runtime Validation)
Hooks intercept tool calls and can block actions before they execute. This is the most flexible control layer.
How Hooks Work
- Claude decides to use a tool (e.g.,
Writeto a file) PreToolUsehook fires with the tool name and arguments as JSON on stdin- Hook script evaluates and returns one of:
Exit code method:
- Exit 0: Allow the action
- Exit 2: Block the action (stderr message shown to Claude as reason)
- Any other exit: Hook error, action proceeds with warning
JSON output method (recommended): Exit 0 and print JSON to stdout with a decision:
{"decision": "block", "reason": "Write outside project not allowed"}
For PreToolUse, you can also use hookSpecificOutput.permissionDecision to return "allow" or "deny".
Configuration
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/validate-file-path.sh"
}
]
},
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/validate-bash-command.sh"
}
]
}
]
}
}
Example: Block Bash Commands That Write Outside CWD
.claude/hooks/validate-bash-command.sh:
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
# Block dangerous patterns
BLOCKED_PATTERNS=(
"rm -rf /"
"rm -rf ~"
"> /etc/"
">> /etc/"
"chmod 777"
"mkfs"
)
for pattern in "${BLOCKED_PATTERNS[@]}"; do
if echo "$COMMAND" | grep -qF "$pattern"; then
echo "BLOCKED: Dangerous command pattern detected: $pattern" >&2
exit 2
fi
done
exit 0
Hook Security Considerations
- Protect hook scripts from Claude: Add hook scripts to permission
denyrules so Claude cannot modify them:{"permissions": {"deny": ["Edit(.claude/hooks/*)", "Write(.claude/hooks/*)"]}} - Timeout: Hooks have a 10-minute default timeout (configurable). A hung hook will eventually time out, and the action proceeds.
- Hook execution context: Hooks run outside the Bash sandbox — they have full system access to perform validation.
- Error handling: A crashing hook (exit code other than 0 or 2) does NOT block the action. Design hooks to fail-closed by defaulting to
exit 2on unexpected errors.
Available Hook Events
| Event | When | Can Block? |
|---|---|---|
PreToolUse | Before any tool executes | Yes |
PostToolUse | After tool succeeds | No |
PostToolUseFailure | After tool fails | No |
UserPromptSubmit | Before processing user input | Yes |
SessionStart | Session begins | No |
Stop | Before Claude stops responding | Yes |
ConfigChange | Settings modified during session | Yes |
SubagentStart | Subagent launched | No |
SubagentStop | Subagent completed | No |
PermissionRequest | Permission prompt shown | No |
Notification | Notification displayed | No |
PreCompact | Before context compaction | No |
SessionEnd | Session ends | No |
Note: This is not exhaustive — check the official hooks docs for the latest event list. Timeout defaults vary: command hooks 10 minutes, prompt hooks 30 seconds, agent hooks 60 seconds.
9. MCP Server File Access
Model Context Protocol (MCP) servers extend Claude Code with external tools. MCP tools can bypass some built-in restrictions.
The Risk
If you configure a filesystem MCP server (e.g., @modelcontextprotocol/server-filesystem), it can:
- Read files that
.claudeignoreblocks - Write files outside the CWD
- Bypass the Bash sandbox (MCP servers run outside the sandbox)
However, MCP tools ARE subject to Claude Code’s permission rules. You can deny specific MCP tools with rules like deny: ["mcp__filesystem__write_file"]. MCP tools also appear in hook events, so PreToolUse hooks can intercept them.
Mitigations
Restrict MCP servers to managed-only (Enterprise):
{ "permissions": { "allowManagedMcpServersOnly": true } }Use permission rules for MCP tools:
{ "permissions": { "deny": ["mcp__filesystem__write_file"] } }Avoid filesystem MCP servers in security-sensitive environments. Use Claude Code’s built-in Read/Write/Edit tools instead — they respect permission rules and sandbox.
10. Enterprise & Admin Controls
Managed Settings (Teams/Enterprise)
Server-managed settings override all user and project settings. They are fetched at authentication time and refreshed periodically.
Key managed-only settings:
| Setting | Purpose |
|---|---|
disableBypassPermissionsMode | Prevent users from enabling bypass mode |
allowManagedPermissionRulesOnly | Only admin-defined permission rules apply |
allowManagedHooksOnly | Only admin-defined hooks execute |
allowManagedMcpServersOnly | Restrict MCP servers to admin-approved list |
sandbox.network.allowManagedDomainsOnly | Only admin-approved domains accessible |
Device Management
| Platform | Tool | Method |
|---|---|---|
| macOS | Jamf, Kandji | com.anthropic.claudecode preference domain |
| Windows | Group Policy | Registry keys |
Headless / CI Mode
Claude Code can run non-interactively. Security posture changes in CI:
# Non-interactive with specific tools allowed
claude --print --allowedTools "Read,Grep,Glob" "analyze this code"
# DANGEROUS: skips all permissions (only use in isolated containers)
claude --print --dangerously-skip-permissions "run tests"
Enterprise CI recommendation:
- Run in containers/VMs with no access to production secrets
- Use
--allowedToolsto whitelist only needed tools - Never use
--dangerously-skip-permissionson shared infrastructure - Mount only the required project directory into the container
Audit & Observability
What exists:
ConfigChangehooks can log configuration changes- OpenTelemetry metrics available for integration
- Hook scripts can log all tool invocations to a file or external service
What does NOT exist:
- No built-in audit log of all files accessed
- No centralized dashboard for file access events
- No compliance-grade audit trail out of the box
DIY audit logging via hooks:
#!/bin/bash
# .claude/hooks/audit-log.sh — log all tool invocations
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name // "unknown"')
ARGS=$(echo "$INPUT" | jq -c '.tool_input // {}')
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) TOOL=$TOOL ARGS=$ARGS" >> .claude/audit.log
exit 0 # Always allow — this is just logging
11. Limitations & Mitigations
Every limitation is paired with an actionable mitigation:
| Limitation | Mitigation |
|---|---|
| Bash can write outside CWD without sandbox (Claude’s policy only, not OS-enforced) | Enable sandbox: "sandbox": {"enabled": true} — OS blocks writes outside CWD |
| CLAUDE.md is advisory only — not enforced | Use permission rules + sandbox + hooks |
| Permission patterns are fragile for Bash commands (variants bypass rules) | Deny the tool entirely or use sandbox network/filesystem restrictions |
| Network sandbox doesn’t inspect HTTPS content | Restrict allowedDomains narrowly; avoid wildcard domains |
.claudeignore doesn’t block Bash cat/head commands | Combine with sandbox denyRead rules |
| Docker/WSL1 not sandboxed | Use WSL2 or run Claude Code in devcontainers |
Env vars exposed via Bash (echo $SECRET) | Don’t store secrets in env vars on dev machines; use vault/secret managers |
| Unmanaged devices: admin users can modify settings | Use managed settings (refreshed hourly); run in managed containers |
| Hook timeout (10 min default) allows action on hang | Keep hook scripts fast; set explicit timeout; fail-closed on errors |
MCP servers bypass .claudeignore and sandbox (but NOT permission rules) | Use allowManagedMcpServersOnly; deny specific MCP tools via permission rules |
| Symlinks can bypass path-based rules | Deny symlink creation in hooks; use sandbox denyWrite on sensitive targets |
| Failing hooks don’t block (non-0, non-2 exit codes allow action) | Design hooks to exit 2 on any unexpected error (fail-closed) |
12. Real-World Scenarios
Scenario 1: Protecting Secrets
A developer has .env, ~/.aws/credentials, and ~/.ssh/ on their machine.
Configuration (.claude/settings.json):
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(~/.aws/*)",
"Read(~/.ssh/*)",
"Bash(cat .env*)",
"Bash(cat ~/.aws/*)",
"Bash(cat ~/.ssh/*)"
]
},
"sandbox": {
"enabled": true,
"filesystem": {
"denyRead": ["~/.aws", "~/.ssh"]
}
}
}
.claudeignore:
.env
.env.*
The gotcha: Bash(cat .env) bypasses the Read(./.env) deny rule because they are different tools. The Bash(cat .env*) deny rule catches the obvious case, but Bash(head -1 .env) or Bash(python -c "open('.env').read()") still work. The sandbox denyRead is the only reliable protection.
Scenario 2: Monorepo Scoping
Team A’s code is in /repo/team-a/, Team B’s is in /repo/team-b/. Claude should only access Team A’s directory.
# Start Claude Code from Team A's directory
cd /repo/team-a
claude
.claude/settings.json in /repo/team-a/:
{
"permissions": {
"deny": [
"Read(//repo/team-b/**)",
"Edit(//repo/team-b/**)",
"Write(//repo/team-b/**)"
]
},
"sandbox": {
"enabled": true,
"filesystem": {
"denyRead": ["//repo/team-b"],
"denyWrite": ["//repo/team-b"]
}
}
}
Scenario 3: Contractor with Temporary Access
A contractor should use Claude Code on /projects/client-x/ but must not access other projects on a shared dev machine.
Setup:
Create a dedicated user or use containers
Start Claude from the project directory:
cd /projects/client-x claude --permission-mode dontAsk.claude/settings.json:{ "permissions": { "defaultMode": "dontAsk", "allow": [ "Read(/src/**)", "Edit(/src/**)", "Bash(npm *)", "Bash(git status)", "Bash(git diff *)" ], "deny": [ "Read(//projects/client-y/**)", "Read(~/*)", "Bash(curl *)", "Bash(wget *)" ] }, "sandbox": { "enabled": true, "allowUnsandboxedCommands": false } }
Scenario 4: Recovering from Misconfiguration
If someone set overly permissive rules, how to audit what Claude accessed:
Step 1: Check if you have audit hooks configured (see Section 10). If yes, review .claude/audit.log.
Step 2: If no audit log exists, there is no built-in way to retroactively see what Claude accessed. Check:
- Git status for unexpected file changes:
git status && git diff - Recently modified files:
find . -mmin -60 -type f - Shell history is not available (Claude uses non-interactive shells)
Prevention: Always deploy the audit hook (Section 10) from day one.
13. Verification & Testing Checklist
After configuring restrictions, verify they work:
Test 1: Verify Read Restrictions
You: Read the file ~/.aws/credentials
Expected: Claude should be blocked by the deny rule or sandbox. You should see a permission denied error.
Test 2: Verify Write Restrictions
You: Create a file at /tmp/test-claude-escape.txt with the content "hello"
Expected: With sandbox enabled, the OS blocks this — /tmp is outside CWD. Without sandbox, Claude’s policy says it won’t write there, but a Bash(echo "hello" > /tmp/test.txt) command would succeed because it’s not OS-enforced.
Test 3: Verify Bash Sandbox
You: Run this command: cat /etc/passwd
Expected: If denyRead includes //etc, the sandbox blocks it. Without sandbox, the command succeeds.
Test 4: Verify Hook Blocks
You: Edit the file /etc/hosts and add a line
Expected: The PreToolUse hook blocks the Edit tool for paths outside the project. You should see the “BLOCKED” message.
Test 5: Verify .claudeignore
You: Read the .env file in this project
Expected: Claude’s Read tool should refuse, stating the file is ignored.
Test 6: Test the Bash Bypass of .claudeignore
You: Run: cat .env
Expected: This will succeed unless sandbox denyRead is configured. This test confirms why .claudeignore alone is insufficient.
Summary: Defense-in-Depth Layers
| Layer | What It Protects | Enforcement | Bypass Risk |
|---|---|---|---|
.claudeignore | Blocks Read tool + indexing | Tool-level | High (Bash bypasses it) |
| Permission rules | Controls which tools access which paths | Policy-based | Medium (Bash variants) |
| Sandbox | OS-enforced: Bash writes restricted to CWD only + network isolation | OS-level | Low (requires escape exploit) |
| Hooks | Runtime validation of any tool call | Script-based | Medium (timeout, crash) |
| Managed settings | Admin-enforced configuration | Server-side | Low (needs device admin) |
Minimum enterprise recommendation: Permission rules + Sandbox + Hooks (all three).
Maximum security: All of the above + managed settings + containerized execution + audit hooks.
Questions or feedback? Reach out on LinkedIn