yapermission rule syntax
A yapermission policy is a single TOML file with up to five top-level keys: default, deny, ask, allow, defer. The four rule-list keys are arrays of tables ([[deny]], [[ask]], etc.).
Top-level shape
default = "ask" # allow | deny | ask | defer (fallback when no rule matches)
# evaluated 1st; matched rule emits permissionDecision "deny"
[[deny]]
# ... rule fields ...
# evaluated 2nd; matched rule emits permissionDecision "ask"
[[ask]]
# ... rule fields ... # forces the prompt even when an [[allow]] rule below matches
# evaluated 3rd; matched rule emits permissionDecision "allow"
[[allow]]
# ... rule fields ...
# evaluated 4th; matched rule emits permissionDecision "defer"
[[defer]]
# ... rule fields ... # hands the decision to the next PreToolUse hook
If no rule matches, the top-level default value is emitted. Missing default means ask.
Each rule-list key directly mirrors Claude Code's permissionDecision field — there are no aliases:
| Decision | Behavior |
|---|---|
allow |
Skip the permission prompt and run the tool |
deny |
Block the tool call (engine attaches permissionDecisionReason) |
ask |
Force the permission prompt (rule's reason is shown to the user) |
defer |
Hand the decision to the next hook in the chain |
The default field accepts only those four values; anything else falls back to ask.
Rule shape
[[allow]]
name = "short-identifier" # appears in audit log; required for clarity
tool = "Bash" # regex matched against tool name; missing = match any
reason = "Read-only operations" # shown to the user on deny/ask; logged on allow/defer
matches = [ # list of input-pattern entries (REQUIRED)
{ command = '^git (status|log)\b' }, # all fields in this entry must match (AND)
{ command = '^ls\b' }, # any entry that matches makes the rule fire (OR)
]
Always use TOML literal strings (single quotes) for regex patterns. They don't process backslash escapes, so '\brm\b' works as intended. Basic strings (double quotes) would require "\\brm\\b" — error-prone, easy to break.
Matching semantics
toolis a regex (re.search).Bashmatches exactly;'Write|Edit'matches either;'^mcp__github__(get_|list_)'matches any read-only github MCP call.matchesis a list of inline tables. The rule fires if any one entry matches.- Within an entry, every
field = patternpair is checked withre.searchagainsttool_input[field]. If a referenced field is absent, it's treated as the empty string. - Use
{ }(empty inline table) to mean "match any input" (e.g., when thetoolregex is enough). - Missing or empty
matchesmeans the rule never fires. To match any input, writematches = [ {} ].
Evaluation order
┌──────────┐ matched ┌──────────────┐
│ deny │ ─────────────►│ return deny │
└────┬─────┘ └──────────────┘
│ no match
▼
┌──────────┐ matched ┌──────────────┐
│ ask │ ─────────────►│ return ask │
└────┬─────┘ └──────────────┘
│ no match
▼
┌──────────┐ matched ┌──────────────┐
│ allow │ ─────────────►│ return allow │
└────┬─────┘ └──────────────┘
│ no match
▼
┌──────────┐ matched ┌──────────────┐
│ defer │ ─────────────►│ return defer │
└────┬─────┘ └──────────────┘
│ no match
▼
┌──────────┐
│ default │
└──────────┘
The order encodes "more restrictive intent wins": deny is absolute, ask forces a manual confirm even when a broader allow rule below would auto-approve, and defer only fires when nothing earlier had an opinion.
Config resolution
- If
./.yapermission.tomlexists in the cwd, it is the only active config (replaces global). - Otherwise,
~/.yapermission.tomlis used. - Otherwise, every call falls through to Claude Code's normal permission prompt.
- If the active TOML fails to parse, the hook logs the error and falls back to
ask(fail-open).
Cookbook
Auto-allow read-only git:
[[allow]]
name = "safe-git-reads"
tool = "Bash"
matches = [
{ command = '^git (status|log|diff|branch|show|remote)\b' },
]
Force a prompt for destructive git, even though git-all below would auto-allow:
[[ask]]
name = "destructive-git"
tool = "Bash"
reason = "Destructive git operation — confirm before running"
matches = [
{ command = 'git push.*--force\b' },
{ command = 'git reset\s+--hard\b' },
{ command = 'git branch\s+-D\b' },
]
[[allow]]
name = "git-all"
tool = "Bash"
matches = [
{ command = '^git\b' },
]
Block destructive commands:
[[deny]]
name = "nuke"
tool = "Bash"
reason = "Destructive command — manual approval required"
matches = [
{ command = '\brm\s+-rf\b' },
{ command = 'sudo\s+rm\b' },
{ command = 'dd\s+.*of=/dev/' },
]
Restrict file writes to a project directory:
[[deny]]
name = "writes-outside-project"
tool = 'Write|Edit'
reason = "Writes outside ~/code/ require manual approval"
matches = [
{ file_path = '^(?!/Users/me/code/)' }, # negative lookahead
]
Allow a whole MCP namespace:
[[allow]]
name = "github-reads"
tool = '^mcp__github__(get_|list_|search_)'
matches = [ {} ] # no input constraints
Defer to a downstream policy hook for sensitive paths:
[[defer]]
name = "sensitive-area"
tool = 'Write|Edit'
reason = "Hand off to the policy-enforcement hook"
matches = [
{ file_path = '^/Users/me/sensitive-area/' },
]
For deeper coverage (regex tips, debugging via the audit log, advanced patterns), see references/schema.md.