Pitfalls
Settings bugs are silent. A misconfigured permission doesn't throw an error — it either blocks something that should work or allows something that shouldn't. These are the failure modes that hit production teams.
The Read/Edit Bypass — The #1 Security Misconception
The problem: Read(./.env) in a deny rule blocks Claude's built-in Read tool. It does NOT block cat .env via Bash.
This is the single most common security misconception in Claude Code configuration. Teams add deny rules for sensitive files, assume they're protected, and leave a cat-sized hole in their security posture.
Demonstration:
{
"permissions": {
"deny": ["Read(./.env)", "Read(./.env.*)"]
}
}Claude cannot use the Read tool on .env. Claude CAN run Bash(cat .env), Bash(grep API_KEY .env), or Bash(base64 .env). The deny rule only applies to the Read tool, not to Bash commands that read the same file.
The fix: Enable sandbox for OS-level enforcement:
{
"permissions": {
"deny": ["Read(./.env)", "Read(./.env.*)"]
},
"sandbox": {
"enabled": true,
"filesystem": {
"denyRead": ["./.env", "./.env.*"]
}
}
}The sandbox blocks the file at the OS level. No tool — built-in or Bash — can read it.
Permission Conflicts Between Scopes
The problem: You allow Bash(npm *) in user settings. The project denies it. The deny wins, and commands fail without an obvious explanation.
The rule: Deny at ANY level blocks the tool at ALL levels. There is no override. A user allow cannot override a project deny. A project allow cannot override a managed deny. A CLI --allowedTools cannot override a managed deny.
User settings: allow Bash(npm *)
Project settings: deny Bash(npm *)
Result: DENIEDDebugging steps:
- Run
/permissions— lists all active rules and which settings file they come from - Run
/status— shows active settings sources, model, account info - Check the deny list first — deny rules are evaluated before everything else
- Check scope precedence — Local > Project > User
Settings That Silently Get Ignored
These settings do nothing unless placed in the correct scope. No error, no warning.
| Setting | Ignored When Placed In... |
|---|---|
allowManagedHooksOnly | User or project settings |
allowManagedMcpServersOnly | User or project settings |
allowManagedPermissionRulesOnly | User or project settings |
channelsEnabled | User or project settings |
forceRemoteSettingsRefresh | User or project settings |
strictKnownMarketplaces | User or project settings |
sandbox.filesystem.allowManagedReadPathsOnly | User or project settings |
sandbox.network.allowManagedDomainsOnly | User or project settings |
autoMode | Shared project settings (.claude/settings.json) |
skipDangerousModePermissionPrompt | Project settings |
autoMemoryDirectory | Project settings |
| MCP server configs | settings.json (belongs in ~/.claude.json or .mcp.json) |
autoConnectIde, editorMode | settings.json (belongs in ~/.claude.json) |
The autoMode case is particularly dangerous. You configure auto mode rules in the project settings, commit them, and wonder why the classifier ignores them. Auto mode reads from user settings and .claude/settings.local.json only — shared project settings are excluded to prevent repo injection of allow rules.
Overly Restrictive Permissions
The problem: Denying too many Bash commands forces Claude to find workarounds or fail repeatedly, wasting tokens and time.
Symptoms:
- Claude keeps asking for permission on every command
- Claude tries alternative approaches to achieve the same goal
- Token usage spikes with no progress
- Claude suggests you run commands manually
The fix: Use acceptEdits or auto mode for development. Reserve dontAsk for production/CI where you know exactly which tools are needed. Prefer sandbox + allow rules over extensive deny lists.
Anti-pattern vs. pattern:
// Anti-pattern: 50 deny rules
{
"permissions": {
"deny": [
"Bash(rm *)", "Bash(mv *)", "Bash(cp *)",
"Bash(chmod *)", "Bash(chown *)", "Bash(ln *)"
// ... 44 more rules
]
}
}
// Pattern: sandbox + targeted allows
{
"permissions": {
"defaultMode": "acceptEdits",
"deny": ["Bash(git push --force *)"]
},
"sandbox": {
"enabled": true,
"filesystem": {
"allowWrite": [".", "/tmp"],
"denyWrite": ["/etc", "/usr/local/bin"]
}
}
}The bypassPermissions Subagent Inheritance Trap
The problem: bypassPermissions skips ALL prompts. If Claude reads a file containing prompt injection, it executes arbitrary commands without confirmation.
The critical gotcha: Subagents inherit bypassPermissions and it CANNOT be overridden. A subagent with permissionMode: plan in its frontmatter still gets full unsupervised access if the parent session uses bypassPermissions.
---
name: safe-reviewer
permissionMode: plan # THIS IS IGNORED
tools: Read, Grep
disallowedTools: Write, Edit, Bash
---If the parent session runs with bypassPermissions, this subagent gets bypassPermissions too. The permissionMode: plan frontmatter is silently ignored. The disallowedTools list is also bypassed.
The fix: Only use bypassPermissions in isolated environments — containers, VMs, ephemeral CI runners. For development, use auto mode or acceptEdits instead.
Bash Pattern Fragility
The problem: Bash permission patterns look specific but match less than you think.
Bash(curl http://github.com/ *) appears to restrict curl to GitHub, but fails for:
- Options before URL:
curl -X GET http://github.com/... - Different protocol:
curl https://github.com/... - Variable expansion:
URL=http://github.com && curl $URL - Redirects:
curl -L http://bit.ly/xyz
The fix: For URL filtering, deny Bash network tools entirely and use WebFetch with domain restrictions:
{
"permissions": {
"deny": ["Bash(curl *)", "Bash(wget *)"],
"allow": ["WebFetch(domain:github.com)"]
}
}WebFetch applies domain restrictions at the application level. Bash patterns are too fragile for URL-based security.
Model Selection Mistakes
The problem: Using Opus for formatting tasks (15x the cost of Haiku) or Haiku for architecture decisions (insufficient reasoning depth).
| Mistake | Cost Impact | Quality Impact |
|---|---|---|
| Opus for simple lookups | 15x overspend | None |
| Haiku for complex debugging | Minimal savings | Significantly worse results |
| Opus for all subagents | Multiplied by subagent count | Marginal improvement |
| No effort level set | Varies | Inconsistent depth |
The fix: Use opusplan as your default. Use subagents with model: haiku for exploration and simple tasks. Switch to opus only for specific hard problems. Set effort levels explicitly — don't rely on defaults.
Configuration Drift Across Teams
The problem: Team members accumulate different user-level permissions from "Yes, don't ask again" prompts. Over weeks, every developer has a different effective configuration, leading to inconsistent behavior and "works on my machine" for AI-assisted workflows.
Detection: Ask team members to run /permissions and compare outputs. If they differ significantly, you have configuration drift.
The fix:
- Define standard permissions in project
.claude/settings.json - Set
permissions.defaultModeto a consistent baseline - For enterprise, use managed settings to enforce critical policies
- Periodically audit
/permissionsoutput across team members - Document expected configuration in CLAUDE.md so deviations are visible
Profile Switching Confusion
The problem: Forgetting which profile is active. Using the work profile with personal API keys, or the personal profile against the corporate proxy.
Symptoms: Wrong model, unexpected permission denials, authentication failures, billing surprises.
The fix: Surface the active profile in your shell prompt:
export PS1="[claude:${CLAUDE_PROFILE:-default}] $PS1"Or use a status line configuration that shows the active config directory.
Windows Path Normalization
On Windows, paths are normalized to POSIX form before matching. C:\Users\alice becomes /c/Users/alice.
Read(//c/**/.env) # Match .env files on C drive
Read(//**/.env) # Match .env files on ALL drivesUse double-slash prefix for absolute paths on Windows. Single-slash is relative to project root.
Auto Mode Default Replacement
The problem: Setting autoMode.soft_deny or autoMode.allow replaces the ENTIRE default list. If you set soft_deny with one custom entry, every built-in safety rule — force push protection, data exfiltration blocking, curl | bash prevention, production deploy guards — becomes allowed.
The fix: Always run claude auto-mode defaults first. Copy the full default lists. Then add your entries to the copied lists.
# Get current defaults
claude auto-mode defaults > auto-mode-defaults.json
# Edit the file, ADD your rules to the existing lists
# Then apply in settingsSubagent Settings Limitations
Settings that you expect to propagate to subagents but don't:
| Setting | Subagent Behavior |
|---|---|
| CLAUDE.md | NOT loaded — subagent only gets frontmatter system prompt |
| Skills | NOT inherited — must be listed explicitly in frontmatter |
| Hooks in settings.json | Run in main session only — subagent hooks go in frontmatter |
permissionMode from frontmatter | Ignored if parent uses bypassPermissions or auto |
Plugin subagent hooks field | Silently ignored |
Plugin subagent mcpServers field | Silently ignored |
Plugin subagent permissionMode field | Silently ignored |
The CLAUDE_CODE_SUBAGENT_MODEL environment variable overrides ALL subagent model settings, including frontmatter. If you set it and a subagent specifies model: opus in frontmatter, the env var wins.
Debugging Checklist
When a permission denial doesn't make sense, follow this sequence:
/permissions— Lists all rules and their source files/status— Shows active settings sources, model, account info- Check deny rules — Deny is evaluated first and always wins
- Check scope precedence — Managed > CLI > Local > Project > User
- Check managed settings — Cannot be overridden by any other scope
- Check auto mode denials — Press
ron denied actions in the "Recently denied" tab to retry - Check sandbox rules —
sandbox.filesystem.denyReadandsandbox.network.allowedDomainsoperate at the OS level, independent of permission rules