Master the tiered permission system, sandboxing, and enterprise security
Claude Code is powerful — it can read, write, and execute anything on your machine. This lesson teaches you how to control that power with precision.
Claude Code uses a tiered permission system to balance power and safety:
| Tool Type | Example | Approval Required | "Don't Ask Again" |
|---|---|---|---|
| Read-only | File reads, Grep | No | N/A |
| Bash commands | Shell execution | Yes | Permanently per project + command |
| File modifications | Edit/write files | Yes | Until session end |
| Mode | Behavior | Use Case |
|---|---|---|
default | Prompts on first use | Normal development |
acceptEdits | Auto-accepts file edits |
Full access
Unlock all 14 lessons, templates, and resources for Claude Code Mastery. Free.
| Fast iteration |
plan | Read-only, no changes | Code analysis |
delegate | Coordination-only | Agent team leads |
bypassPermissions | Skips all prompts ⚠️ | Sandboxed/CI environments |
Switch modes during a session with Shift+Tab.
Q: When you click "Yes, don't ask again" for a Bash command, how long does that permission last?
Permanently per project directory. For Bash commands, "don't ask again" persists across sessions for that specific project + command combination. For file edits, it only lasts until the session ends.
deny → ask → allow
First matching rule wins. Deny always takes precedence.
{
"permissions": {
"allow": [
"Bash(npm run *)",
"Bash(git commit *)",
"Bash(git status *)",
"Bash(pnpm *)"
],
"deny": [
"Bash(git push *)",
"Bash(rm -rf *)",
"Bash(curl *)",
"Bash(wget *)",
"Edit(.env)"
]
}
}
Wildcards (*) can appear anywhere in the command:
| Rule | Matches |
|---|---|
Bash(npm run build) | Exact command |
Bash(npm run *) | npm run test, npm run build, etc. |
Bash(* --version) | node --version, python --version, etc. |
Gotcha:
Bash(ls *)matchesls -labut notlsof. The space matters —Bash(ls*)(no space) matches both.
Follow gitignore-style patterns:
| Pattern | Meaning |
|---|---|
Read(//Users/alice/secrets/**) | Absolute path |
Read(~/Documents/*.pdf) | Home directory relative |
Edit(/src/**/*.ts) | Relative to settings file |
"allow": ["mcp__github__create_pull_request"],
"deny": ["mcp__puppeteer__*"]
Fill in the blanks:
___ → ask → allowmcp__servername________denymcp__servername__*acceptEditsSandboxing provides OS-level enforcement that restricts Bash commands' filesystem and network access.
Filesystem Isolation:
Network Isolation:
OS-Level Enforcement:
| OS | Technology |
|---|---|
| macOS | Seatbelt (built-in) |
| Linux | bubblewrap + socat |
/sandbox
| Mode | Behavior |
|---|---|
| Auto-allow | Sandboxed commands run without permission prompts |
| Regular | All commands go through permission flow, even sandboxed |
Pro Tip: Auto-allow mode with sandboxing is the sweet spot — fast iteration with OS-level safety guarantees.
Q: With sandboxing enabled, what happens if Claude tries to modify ~/.bashrc?
The modification is blocked at the OS level. Sandboxing restricts write access to the current working directory and its subdirectories. Even if Claude is tricked via prompt injection, it physically cannot modify system files like ~/.bashrc, ~/.zshrc, or any file outside the project.
curl and wget blocked by default/bugQ: Why are curl and wget blocked by default?
They're blocked to prevent data exfiltration. A prompt injection attack could trick Claude into sending sensitive data (API keys, source code, etc.) to an attacker's server. Blocking these commands by default prevents that attack vector.
Claude Code provides a reference devcontainer for maximum isolation:
# Clone the reference implementation
git clone https://github.com/anthropics/claude-code
# Open in VS Code → "Reopen in Container"
Inside a devcontainer, you can safely use:
claude --dangerously-skip-permissions
The container's firewall restricts network to necessary services only, and filesystem access is limited to the project.
For organizations that need centralized control:
{
"model": "sonnet",
"permissions": {
"defaultMode": "default",
"deny": [
"Bash(curl *)",
"Bash(wget *)",
"Bash(rm -rf *)",
"Edit(*.env*)",
"Edit(*.pem)",
"Edit(*.key)"
]
},
"disableBypassPermissionsMode": "disable",
"allowManagedPermissionRulesOnly": true
}
Deploy to:
/Library/Application Support/ClaudeCode/managed-settings.json/etc/claude-code/managed-settings.jsonFill in the blanks:
/______ for OS-level sandboxing/___/sandboxSeatbelt/bug.claude/settings.json{
"permissions": {
"allow": ["Bash(pnpm *)"],
"deny": ["Bash(rm -rf *)"]
}
}
pnpm test — it should auto-approverm -rf node_modules — it should be blocked/sandbox to explore sandbox settingsReflection: How does the permission system change your comfort level with Claude making changes?
Scenario: A junior developer on your team accidentally gave Claude "don't ask again" permission for git push to main. How do you prevent this from happening again?
Add a deny rule to the shared project settings (.claude/settings.json):
{
"permissions": {
"deny": ["Bash(git push * main)", "Bash(git push origin main)"]
}
}
Deny rules always win — even if a user previously allowed the command. For enterprise teams, use managed settings with allowManagedPermissionRulesOnly: true to prevent users from adding their own allow rules.
| Concept | One-Liner |
|---|---|
| Permission tiers | Read (auto), Bash (per-command), Edits (per-session) |
| Evaluation order | deny → ask → allow (deny always wins) |
| Mode switching | Shift+Tab cycles modes mid-session |
| Sandboxing | OS-level filesystem + network isolation |
| macOS sandbox | Seatbelt; Linux uses bubblewrap |
| Default blocks | curl and wget blocked to prevent exfiltration |
| Enable sandbox | /sandbox command |
| Enterprise | Managed settings for org-wide policies |
Next up: Hooks — Automate Everything → — Learn how to trigger shell commands at every lifecycle event.