Claude Code uses a permission-based model:
- Default: Read-only, asks permission for modifications
- Configurable: Pre-allow or deny specific operations
- Scoped: User-level, project-level, or managed
// In settings.json
{
"permissions": {
"allow": [...],
"deny": [...]
}
}{
"allow": ["Read", "Grep", "Glob"],
"deny": ["Write"]
}{
"allow": [
"Bash(git status:*)",
"Bash(npm test:*)",
"Bash(ls:*)"
]
}Format: Tool(pattern:description_pattern)
| Pattern | Matches |
|---|---|
Bash(git *:*) |
All git commands |
Bash(*test*:*) |
Commands containing "test" |
Edit(*.ts:*) |
Edit any TypeScript file |
Task(*) |
All subagent invocations |
Task(test-runner) |
Specific subagent only |
- Deny rules take precedence over allow
- More specific rules override general rules
- Project settings override user settings
- Managed settings override all
These commands are auto-allowed (read-only):
cat,head,tail,lessls,find,treeecho,printfgit status,git log,git diffpwd,whoami,date
{
"permissions": {
"allow": [
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(git push:*)",
"Bash(npm *:*)",
"Bash(yarn *:*)",
"Bash(pnpm *:*)"
]
}
}{
"permissions": {
"allow": ["Read", "Grep", "Glob"],
"deny": ["Write", "Edit", "Bash"]
}
}{
"permissions": {
"deny": [
"Bash(rm -rf:*)",
"Bash(sudo *:*)",
"Bash(*prod*:*)",
"Edit(.env*:*)",
"Write(.env*:*)"
]
}
}MCP tools follow pattern: mcp__servername__toolname
{
"permissions": {
"allow": [
"mcp__github__list_prs",
"mcp__github__pr_review"
],
"deny": [
"mcp__github__delete_*"
]
}
}Allow or deny all tools from an MCP server:
{
"permissions": {
"allow": ["mcp__filesystem__*"],
"deny": ["mcp__dangerous_server__*"]
}
}Disable specific subagents:
{
"permissions": {
"deny": ["Task(dangerous-agent)"]
}
}-
Review current permissions
cat ~/.claude/settings.json | jq '.permissions' cat .claude/settings.json | jq '.permissions'
-
Check for overly permissive rules
Bash(*)allows all commands*in allow is dangerous
-
Verify deny rules are effective
- Test that blocked operations are actually blocked
- Check for bypasses via other tools
-
Review subagent permissions
- Subagents inherit parent permissions by default
- Can be restricted via
toolsfield
-
Check MCP server permissions
- Review each connected MCP server
- Audit allowed tools per server
Claude Code 2.1.3 detects and warns about unreachable permission rules.
- A deny rule that comes after a broader allow (never evaluated)
- Duplicate rules (same pattern twice)
- Rules shadowed by earlier broader rules
{
"permissions": {
"allow": ["Bash(*)"], // Allows ALL bash commands
"deny": ["Bash(rm -rf:*)"] // UNREACHABLE - never evaluated!
}
}Reorder rules or use more specific allows:
{
"permissions": {
"allow": [
"Bash(git *:*)",
"Bash(npm *:*)"
],
"deny": ["Bash(rm -rf:*)"]
}
}Patterns use glob-style matching (not regex):
*matches any characters- Patterns are case-sensitive
- Colon (
:) separates command from description
| Wrong | Why It Fails | Correct |
|---|---|---|
Bash(git:*) |
Matches only "git", not "git status" | Bash(git *:*) |
Bash(npm install:*) |
Won't match "npm install lodash" | Bash(npm install*:*) |
Edit(.env) |
Missing description pattern | Edit(.env:*) |
Bash(git (status|log):*) |
Regex syntax doesn't work | Bash(git *:*) |
To verify a pattern works:
- Add pattern to allow list
- Try the command in Claude Code
- If prompted for permission, pattern didn't match
- Adjust and retry
- Always include
:*for parameterized tools - Use
*sparingly - prefer specific patterns - Test patterns before relying on them
- Review patterns after Claude Code updates
| Issue | Cause | Fix |
|---|---|---|
| Permission prompts despite allow | Typo in pattern | Check exact syntax |
| Can't deny specific command | Allow rule too broad | Make allow more specific |
| Subagent bypasses restrictions | Missing Task deny | Add Task(agent-name) to deny |
| Unreachable rule warning | Rule order issue | Reorder or narrow broader rules |
| Pattern doesn't match | Missing :* or wrong syntax |
See Pattern Validation Guide above |