Skip to the content.

Permissions Quick Reference

This doc is a concise reference for the permissions rule format.

Rules live in .claude/permissions.yaml (project) or ~/.claude/permissions.yaml (global). You can also split rules across .claude/permissions.d/*.yaml / *.yml (or the same under ~/). Configuration is reloaded automatically on the next hook run.

Rule load order

  1. Built-ins (cd, export, empty-command, env-prefix handling)
  2. ~/.claude/permissions.yaml~/.claude/permissions.d/* (alphabetical)
  3. .claude/permissions.yaml.claude/permissions.d/* (alphabetical)

All matching rules combine with strictest wins. A deny short-circuits remaining rules at that node, so an earlier-layer deny beats a later-layer allow.

Decisions

decide: allow | ask | deny | abstain – strictest wins: deny > ask > allow > abstain

Patterns

AND vs OR

List form

bash, read / write / edit / multi_edit, and redirect.out / redirect.in accept a list of rules. webfetch, Grep, and generic tool keys are a single object (not a list).

write:
  - path-in: ["**/.env*", "~/.ssh/*"]
    decide: deny
  - decide: allow   # catch-all

Under bash, list form also works at the command or subcommand level:

bash:
  git:
    - push:
        decide: deny
    - add:
        decide: ask
    - decide: deny    # catch-all: matches any git command not matched above
      reason: No other git commands

Command descriptors

Place YAML files in ~/.claude/permissions.d/commands/<command>.yaml (global) or .claude/permissions.d/commands/<command>.yaml (project). The project layer wins on conflict. Without a descriptor, all flags default to arity 0 (boolean) and no positionals are typed as paths.

# .claude/permissions.d/commands/kubectl.yaml
kubectl:
  description: Kubernetes CLI
  flags:
    context:
      arity: 1      # consumes next token as value
      kind: string
    n|namespace:
      arity: 1
      kind: string
  positionals:
    - kind: string   # first positional (subcommand)
    - kind: path
      variadic: true # remaining positionals are paths

Flag arity: 1 means the flag takes a value; arity: 0 means it is boolean. Use short|long to declare both forms together.

Bash

Keys nest: bash > command > subcommand. Each rule level consumes one positional word from the command line. cmd inside a nested rule addresses args after the subcommand word.

bash:
  sudo:
    decide: deny

  git:
    status: { decide: allow }
    push:
      decide: ask
      reason: Confirm push

  docker:
    compose:
      build:
        decide: ask   # cmd here matches args after "build", not "compose" or "build"

Bash fields

All fields in a rule are AND’d.

Field Semantics
cmd: "src/** dist/**" Positional args matched by index; space-separated or array; AND
cmd-in: [A, B] Any positional arg matches any entry; OR
options: [r\|recursive, f\|force] All flags present; x\|long matches either short or long form; AND
options-in: [force, force-with-lease] Any flag present; OR
options: {m\|message: "/wip/"} Flag with specific value
env: {CI: "true"} All env vars match; AND
cwd: $/** cwd matches pattern; $ and $ expand when set
path: ... Synonym for cwd on bash entries
cwd-in: [/etc/**, /usr/**] cwd matches any entry; OR
file: {"~/.kube/config": true} File exists
file: {"~/.kube/config": {contains: "current-context: sandbox"}} File exists and contains pattern

Examples on a rule

Any rule with decide can list example calls under the decision each one should produce. The engine ignores them; bun run check-config <config-dir> collects them and decides each one with the engine, failing when the decision differs.

Examples are decided against a stand-in project directory, /project, not the config directory’s parent. It does not have to exist, so examples read the same on every machine.

bash:
  terraform:
    cwd: "$/**"
    decide: allow
    examples:
      allow:
        # String form: a command, run in the stand-in project directory.
        - terraform plan
      ask:
        # Object form: a command plus the working directory it runs in.
        - cmd: terraform plan
          cwd: ../outside-the-project

read:
  path: "$/**"
  decide: allow
  examples:
    allow:
      # Read/Write/Edit match file_path exactly as given, so name it in full.
      - read /project/src/index.ts

Full details in CONFIGURATION.md.

Redirect path rules

Shell redirects write to or read from file paths. Match those paths globally without a separate rule for each command (echo, tee, cat, and similar).

redirect:
  out:   # >, >>, 2>, &>
    - path-in: ["/tmp/**", "$/**"]
      decide: allow
    - decide: ask
      reason: Shell write outside allowed dirs
  in:    # <
    - path-in: ["/tmp/**", "$/**"]
      decide: allow
    - decide: ask
      reason: Shell read outside allowed dirs

not:

not: is a bash-only field. It inverts matcher fields on that bash entry. The example below denies all aws commands when AWS_PROFILE is anything other than sandbox:

bash:
  aws:
    - not:
        env:
          AWS_PROFILE: sandbox
      decide: deny
      reason: Blocked outside sandbox

File tools (read, write, edit, multi_edit)

Fields: path / path-in, optional cwd, decide, reason, and nested rules: (parent may carry cwd). No env or not: on file-tool entries.

read:
  path: "**/.env*"
  decide: ask

write:
  - path-in: ["**/.env*", "~/.ssh/*"]
    decide: deny
  - decide: allow   # catch-all

WebFetch

Single object with optional host / host-in, plus decide and optional reason. Not a list. Omit both host fields to match every WebFetch URL.

webfetch:
  host-in: [docs.anthropic.com, "*.github.com"]
  decide: allow

Grep

Single object with decide and optional reason. Matches every Grep tool call (no path/cwd matchers).

Grep:
  decide: allow

Generic tool rules

Top-level keys that are not recognised sections are matched against the Claude Code tool name. Quote keys containing glob chars. Use tool or tool-in when the key is only a label. Single object only (not a list).

ToolSearch:
  decide: allow

"mcp__*__delete_*":
  decide: deny

github-writes:
  tool-in: [mcp__github__create_issue, mcp__github__create_pull_request]
  decide: ask

Nested rules

On bash entries, rules: groups sub-rules under shared parent conditions. The parent needs no decide – it is a pure filter:

bash:
  aws:
    - env:
        AWS_PROFILE: /^(?!sandbox$)/
      rules:
        - cmd: "* delete-*"
          decide: deny
        - decide: ask
          reason: Confirm on non-sandbox profile

Troubleshooting