12.2 Permissions, Modes & Hooks

Key Takeaways

  • Permission rules take the form Tool or Tool(specifier), such as Bash(pnpm test:*) or Read(./.env), and live in the permissions object's allow, ask, and deny arrays.
  • Permission modes are default (aliased manual), acceptEdits, plan, auto, dontAsk, and bypassPermissions; dontAsk is the strictest because it auto-denies anything not pre-approved.
  • Hook events are PascalCase and specific - PreToolUse, PostToolUse, SessionStart, SubagentStop, PreCompact - and are configured three levels deep as event, then matcher group, then handler.
  • Only exit code 2 blocks a hooked action; exit 0 succeeds and other non-zero codes are non-blocking errors that let the action proceed unless decision JSON says otherwise.
  • Headless runs use claude --print with --output-format, --permission-mode, --allowedTools, and --max-turns; --dangerously-skip-permissions is equivalent to --permission-mode bypassPermissions.
Last updated: September 2026

Permissions, Modes & Hooks

Exam Blueprint Focus: This section carries two separately weighted sub-skills: Claude Code Operation (3.1%) and, inside the Security and Safety domain, Claude Hooks (1.0%). Both are graded on exact names — the permission rule syntax, the mode names, and the hook event names. Approximate answers are wrong answers here.

The Permission Problem

An autonomous coding agent with shell and filesystem access creates a direct tension:

  • Confirm every read and the agent is unusable for routine work.
  • Confirm nothing and one misread instruction can drop a database, overwrite uncommitted work, or force-push to main.

Claude Code resolves this with a tiered permission system plus explicit rules you write.

Tool typeExampleApproval required in Manual mode
Read-onlyRead, Grep, GlobNo, within the working directory and additionalDirectories
Bash commandsShell executionYes, except a built-in set of read-only commands
File modificationEdit, WriteYes, until the session ends or a rule allows it

Permission Rule Syntax

Every rule has the form Tool or Tool(specifier). Parentheses inside a specifier are literal and need no escaping.

RuleEffect
BashAll Bash commands (Bash(*) is equivalent)
Bash(npm run build)Exactly that command
Bash(pnpm test:*)Any command matching the prefix pattern
Read(./.env)Reading .env in the current directory
Edit(src/**)Edits under src/
WebFetch(domain:example.com)Fetches to that domain
mcp__github__create_issueOne tool from an MCP server
Agent(model:opus)Subagent calls requesting the Opus tier

Rules live in three arrays under permissions:

{
  "permissions": {
    "allow": ["Bash(pnpm test:*)", "Read(src/**)"],
    "ask":   ["Bash(git push:*)"],
    "deny":  ["Read(./.env)", "Bash(curl:*)"],
    "defaultMode": "acceptEdits",
    "additionalDirectories": ["../shared-lib"]
  }
}

Behaviour that is directly testable:

  • deny and ask rules apply immediately. allow rules and additionalDirectories wait until the folder is trusted.
  • An allow rule in your local file does not outrank an ask rule from project or managed settings. This is why "Yes, and don't ask again" sometimes still prompts.
  • List keys merge across scopes. Your personal allow entries add to the team's; they do not replace them.
  • A deny rule with no specifier removes the tool from Claude's context entirely, so the model does not even see it as an option.

High-yield trap: allowedTools, confirmTools, and blockedCommands are not settings.json keys. --allowedTools and --disallowedTools exist as CLI flags, but the file-based configuration is the permissions object only.


Permission Modes

The mode sets the baseline behaviour before rules are consulted. Set it with permissions.defaultMode in settings, or --permission-mode for one session.

ModeBehaviour
default (aliased manual)Prompts on first use of each tool
acceptEditsAuto-accepts file edits and common filesystem commands (mkdir, touch, mv, cp) inside the working and additional directories
planClaude explores with reads and read-only shell commands but does not edit source files
autoAuto-approves tool calls with background safety classification instead of human review
dontAskAuto-denies anything not pre-approved by an allow rule
bypassPermissionsSkips prompts entirely, except for the small set of actions no mode auto-approves

Two nuances worth carrying into the exam. dontAsk is not a permissive mode — it is the strictest, failing closed rather than prompting, which makes it the right choice for unattended automation with a curated allowlist. And bypassPermissions is the dangerous one: it skips prompts even for writes to protected paths such as .git and .claude, so it belongs only in a container or VM. Organizations can forbid it with permissions.disableBypassPermissionsMode (and disableAutoMode for auto) in managed settings, where individuals cannot override it.


Interactive vs. Headless Operation

Interactive mode

The default when you run claude in a terminal. You see a syntax-highlighted unified diff before an Edit applies, the exact command string before a Bash call runs, and an approval prompt offering yes / no / "yes, and don't ask again" / abort.

Headless mode

Headless runs are driven by --print (-p), not by an --auto-pilot flag:

claude -p "Fix the failing tests in src/billing and commit the result" \
  --output-format json \
  --permission-mode dontAsk \
  --allowedTools "Read" "Edit" "Bash(pnpm test:*)" \
  --model claude-sonnet-5 \
  --max-turns 8
  • --output-format takes text, json, or stream-json — use json when a CI step needs to parse the result.
  • --max-turns caps the agentic loop, the primary defence against a runaway agent burning budget.
  • --dangerously-skip-permissions is equivalent to --permission-mode bypassPermissions. The name is the warning.

Because nobody is watching, headless deployments need three safeguards: an ephemeral sandbox (container or VM, no host credentials), a scoped allowlist paired with dontAsk so anything unlisted fails closed, and fail-fast budgets via --max-turns and timeouts.


Hooks: Deterministic Lifecycle Governance

Prompts steer a probabilistic model. Hooks execute regardless of what the model decides, which is what makes them the enforcement layer that CLAUDE.md is not.

Event names

Hooks are keyed by event name in settings.json. The names are PascalCase and specificPreToolUse, not preTool. The events you should know:

EventFires
SessionStartA session begins or resumes
UserPromptSubmitBefore Claude processes a prompt
PreToolUseBefore a tool call executes
PermissionRequestWhen a tool needs a permission decision
PostToolUseAfter a tool call succeeds
PostToolUseFailureAfter a tool call fails
SubagentStart / SubagentStopA subagent is spawned / finishes
PreCompact / PostCompactAround context compaction
StopClaude finishes responding
SessionEndThe session ends

Configuration shape

The structure is three levels deep: event → matcher group → handler.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/guard.sh",
            "timeout": 30
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/format.sh" }
        ]
      }
    ]
  }
}

A matcher may be an exact tool name, a pipe- or comma-separated list, or a regex such as mcp__.*. Omitting it (or using "*") matches everything. Besides type: "command", handlers can be http, mcp_tool, prompt, or agent.

Hook input and the exit-code contract

The handler receives a JSON payload on stdin. Tool events carry tool_name, tool_input, and tool_use_id, alongside common fields such as session_id, cwd, and permission_mode.

The exit-code semantics are precise, and "any non-zero blocks" is wrong:

  • Exit 0 — success. If stdout is a JSON object it is parsed for decision fields; otherwise it is treated as text.
  • Exit 2blocking error. The action is blocked on events that support blocking, with the reason taken from JSON or stderr.
  • Other non-zero codes — a non-blocking error. The action proceeds unless valid decision JSON says otherwise.

The explicit, reliable way to block is the JSON decision object:

#!/usr/bin/env bash
set -euo pipefail
PAYLOAD=$(cat)
TOOL=$(echo "$PAYLOAD" | jq -r '.tool_name // empty')

if [[ "$TOOL" == "Bash" ]]; then
  CMD=$(echo "$PAYLOAD" | jq -r '.tool_input.command // empty')
  BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)

  if [[ "$BRANCH" == "main" && "$CMD" == git\ commit* ]]; then
    jq -n '{hookSpecificOutput: {hookEventName: "PreToolUse",
            permissionDecision: "deny",
            permissionDecisionReason: "Commits on main are blocked; branch first."}}'
    exit 0
  fi

  if echo "$CMD" | grep -iqE '(api[_-]?key|password|secret|bearer)'; then
    echo "Command may expose a credential." >&2
    exit 2
  fi
fi
exit 0

permissionDecision accepts "deny", "allow", or "escalate". A deny reason is fed back to Claude as tool feedback, so it can choose a different approach — which is why a blocking hook should always explain itself. A hook that blocks silently sends the model into a retry loop that burns tokens on the same rejected action.

Post-tool hooks

PostToolUse fires after a tool succeeds and is where formatters and linters belong: run Prettier, Black, or gofmt on the file that was just written, so what lands on disk is always formatted. Note the ordering hazard — if a PostToolUse hook reformats a file and shifts line numbers, a subsequent Edit built on stale offsets can fail or corrupt adjacent lines.


Skills, Slash Commands, and Subagents

Three extension mechanisms sit alongside permissions and hooks, and CCDV-F expects you to tell them apart.

  • Skills are reusable procedures in SKILL.md files under .claude/skills/<name>/ (project) or ~/.claude/skills/<name>/ (personal). They use progressive disclosure: only the description sits in context, and the body loads when the skill is invoked — automatically when the description matches, or manually as /skill-name.
  • Slash commands invoke a fixed action by name, including user-invocable skills.
  • Subagents are defined in .claude/agents/ and run a delegated task in an isolated context window.

Why subagents matter architecturally

+-------------------------------------------------------------------------+
|                     Primary Orchestrator Agent                          |
|  Task: "Refactor all deprecated auth endpoints"                         |
|  Context: clean, high-level architecture only                           |
+------------------------------------+------------------------------------+
                                     | spawns subagent
                                     v
+------------------------------------+------------------------------------+
|                  Search Subagent (isolated context)                     |
|  Scope: find all 24 usages of verifyAuthToken()                         |
|  Tools: Read, Glob, Grep only - no Bash, no Edit                        |
|  Reads 500 files, processes 15,000 lines                                |
+------------------------------------+------------------------------------+
                                     | returns summary only
                                     v
|  Orchestrator receives: "3 files need updates: a.ts, b.ts, c.ts"        |
|  The 15,000 lines of raw output never enter the parent context          |
+-------------------------------------------------------------------------+
  1. Context hygiene. Raw search output stays quarantined in the subagent; only the synthesis returns.
  2. Least privilege. A read-only exploration subagent gets Read, Glob, and Grep and nothing else.
  3. Parallelism. Independent subproblems investigate concurrently.

A skill with context: fork in its frontmatter combines both: skill modularity plus subagent isolation.


Common Traps

  1. Using lowercase hook names. preTool and postTool are not events. The names are PreToolUse and PostToolUse.
  2. Assuming any non-zero exit blocks. Only exit 2 blocks. Exit 1 or 3 is a non-blocking error and the action proceeds.
  3. Blocking without a reason. Emit permissionDecisionReason, or write to stderr, so the model can adapt instead of retrying.
  4. Unrestricted bypassPermissions on a real workstation. It skips prompts even for .git and .claude writes. Containers only.
  5. Confusing dontAsk with a permissive mode. It auto-denies anything not pre-approved — the safest headless default, not the loosest.
  6. Over-privileged subagents. Exploration subagents should be read-only unless the delegated task genuinely requires writes.
  7. Putting a procedure in CLAUDE.md instead of a Skill. CLAUDE.md loads every session; a Skill loads only when invoked.
Loading diagram...
Permission and Hook Evaluation Order for a Tool Call
Test Your Knowledge

A PreToolUse hook must block a Bash command that would push to main. The author writes the check, prints an explanation to stderr, and calls exit 1. Testing shows the push still happens. What is wrong?

A
B
C
D
Test Your Knowledge

A CI job runs Claude Code unattended to fix failing tests. The team wants anything not explicitly pre-approved to fail rather than prompt or auto-run. Which invocation matches that requirement?

A
B
C
D
Test Your Knowledge

A team keeps a 400-line release-verification procedure in CLAUDE.md. Sessions have grown slow, and Claude increasingly ignores unrelated project conventions. Which change best addresses both symptoms?

A
B
C
D