Permission Precedence
Claude Code uses a layered permission system. Understanding how these layers interact prevents unexpected behavior when combining allow, ask, and deny rules.
Permission Layers
Section titled “Permission Layers”Permissions are defined at three levels:
| Layer | Location | Scope |
|---|---|---|
| Tool-level | permissions.allow, permissions.ask, permissions.deny | Entire tool (e.g., Bash) |
| Content-level | permissions.allow, permissions.ask, permissions.deny | Specific tool invocations (e.g., Bash(rm *)) |
| Skill frontmatter | allowed-tools: in SKILL.md | Tools available within a skill |
Precedence Rules
Section titled “Precedence Rules”Content-level rules override tool-level rules. More specific patterns take priority over less specific ones.
The evaluation order is:
1. deny (content-level) ← highest priority2. ask (content-level)3. allow (content-level)4. deny (tool-level)5. ask (tool-level)6. allow (tool-level) ← lowest priorityExample: allow + ask Interaction
Section titled “Example: allow + ask Interaction”Consider this configuration in .claude/settings.json:
{ "permissions": { "allow": ["Bash"], "ask": ["Bash(rm *)"] }}Behavior: All Bash commands run without prompting, except commands matching rm *, which trigger a permission prompt.
| Command | Rule Matched | Result |
|---|---|---|
Bash(ls -la) | allow: ["Bash"] (tool-level) | Runs automatically |
Bash(npm test) | allow: ["Bash"] (tool-level) | Runs automatically |
Bash(rm -rf dist/) | ask: ["Bash(rm *)"] (content-level) | Prompts for approval |
Bash(rm temp.txt) | ask: ["Bash(rm *)"] (content-level) | Prompts for approval |
The content-level ask for Bash(rm *) overrides the tool-level allow for Bash because content-level rules are more specific.
Note: Prior to Claude Code v2.1.27,
allow: ["Bash"]would allow all Bash commands regardless of content-levelaskrules. This was fixed so that content-level rules always take precedence.
Configuration Locations
Section titled “Configuration Locations”| File | Applies To | Managed By |
|---|---|---|
~/.claude/settings.json | All projects (user-level) | User |
.claude/settings.json | This project | sequant (package-managed) |
.claude/.local/settings.json | This project (overrides) | User |
SKILL.md frontmatter | Within a specific skill | Skill author |
Project settings (.claude/settings.json)
Section titled “Project settings (.claude/settings.json)”{ "permissions": { "deny": ["Read(./.entire/metadata/**)"] }}Skill frontmatter (allowed-tools:)
Section titled “Skill frontmatter (allowed-tools:)”---allowed-tools: - Bash(npm run deploy:*) - Bash(gh workflow run:*)---Skill allowed-tools pre-approve tools while the skill is active: a matching command runs without a permission prompt. They do not override project or user-level deny rules.
Sequant’s own skills grant only the commands their instructions run. No skill grants bare Bash, Bash(*), a wildcard straight after a tool such as Bash(git *), or a runner wildcard such as Bash(npx tsc:*). Since #1292, no skill pre-approves Bash(<tool>:*) for gh api, rm, mv, find, sed or cp either: /qa no longer grants gh api (which also allowed gh api -X DELETE), /clean no longer grants rm or find and moves files only into docs/archive/ and scripts/archive/, and /setup grants cp, cat and sed only for the files it writes or reads. src/lib/__tests__/skill-allowed-tools-gate.test.ts enforces this (#1287, #1292). When you run a skill by hand, any other command it needs asks for permission first. Phases started by sequant run use bypassPermissions, so they do not prompt.
Common Patterns
Section titled “Common Patterns”Allow a tool but restrict dangerous operations
Section titled “Allow a tool but restrict dangerous operations”{ "permissions": { "allow": ["Bash"], "ask": ["Bash(rm *)", "Bash(git push *)"], "deny": ["Bash(rm -rf /)"] }}Deny specific file reads
Section titled “Deny specific file reads”{ "permissions": { "deny": ["Read(.env)", "Read(.env.local)", "Read(**/credentials.*)"] }}Orchestrator-only guards
Section titled “Orchestrator-only guards”Some pre-tool.sh refusals apply only when a phase agent runs under sequant run (the SEQUANT_ORCHESTRATOR environment variable is set); interactive sessions are untouched.
- Background tasks are refused — a
Monitortool call, or a Bash call withrun_in_background: true, is blocked withHOOK_BLOCKED: Background tasks never notify a phase agent. Inside a phase the completion notification never arrives: the phase ends the moment the agent’s turn ends, and any work still uncommitted is stranded. Run long commands in the foreground with a timeout instead (timeout 600 npm test 2>&1 | tail -80), after a WIP commit. If an exec phase still ends with uncommitted changes, sequant commits them aschore(#N): wip checkpoint …before reporting the failure.
See Also
Section titled “See Also”- Customization Guide — overriding settings safely
- Claude Code documentation — upstream permission reference