Claude Certification Guides
CCAR-F/Domain 310 min read

Lesson 3.3

Claude Code Hooks

The hook events Claude Code fires, how a hook receives its input and returns a decision, and the patterns hooks are good for.

Hooks are shell commands (or prompts, or small agents) that Claude Code runs automatically at defined points in a session. Unlike instructions in CLAUDE.md, a hook always runs; the model cannot decide to skip it. That makes hooks the primary mechanism for deterministic enforcement, formatting, auditing and context injection. The exam tests the event names, the input and output contract, and the meaning of exit codes.

Events

EventWhen it firesCan it block?
SessionStartA session begins or resumesNo; stdout becomes context
UserPromptSubmitThe user submits a prompt, before Claude sees itYes; can also add context
PreToolUseBefore a tool executesYes; can allow, deny, ask or rewrite input
PermissionRequestA permission prompt is about to be shownYes; can answer it
PostToolUseAfter a tool succeedsFeedback only; can add context
NotificationClaude Code sends a notification (waiting for input, etc.)No
StopThe main agent is about to finish its turnYes; can force it to continue
SubagentStopA subagent finishesYes, for the subagent
PreCompactBefore context compaction (manual or automatic)No; can add instructions
SessionEndThe session endsNo; cleanup and logging

Further events exist for setup, configuration changes, worktree lifecycle and multi-agent coordination; the ten above are the ones to know cold.

Configuration

Hooks live under hooks in any settings file, grouped by event, then by matcher.

{
  "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": "npx prettier --write \"$CLAUDE_FILE_PATHS\"" } ]
      }
    ],
    "Stop": [
      { "hooks": [ { "type": "prompt", "prompt": "Did the agent run the tests? Answer with a decision." } ] }
    ]
  }
}

The matcher is a regular expression matched against the tool name for tool events (Bash, Edit|Write, mcp__github__.*); an empty or missing matcher, or *, matches everything. Events without a tool (such as Stop) ignore matchers or use them for sub-types (for example the compaction trigger or the session-start source). type: "command" runs a shell command; type: "prompt" asks a model to evaluate a yes/no question against the event; newer versions add agent-style hooks that can use tools. Hooks run in parallel within an event and have a default timeout of 60 seconds. Because hooks execute arbitrary code, Claude Code snapshots them at startup and asks you to review changes made by an external editor through the /hooks menu before they take effect.

Input and output contract

A command hook receives a JSON object on stdin containing common fields (session_id, transcript_path, cwd, hook_event_name) plus event-specific ones: tool_name, tool_input and, for post-tool events, tool_response; prompt for prompt submission; trigger for compaction. Environment variables such as CLAUDE_PROJECT_DIR are also available.

The hook communicates back through its exit code and, optionally, JSON on stdout.

Exit codeMeaning
0Success. For UserPromptSubmit and SessionStart, stdout is added to Claude's context; for other events stdout is shown to the user only
2Blocking error. stderr is fed back to Claude as the reason, and the action is prevented (tool not run, prompt not processed, stop not permitted)
Other non-zeroNon-blocking error. stderr is shown to the user and execution continues

For finer control a hook exits 0 and prints JSON:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "ask",
    "permissionDecisionReason": "Pushing to a protected branch",
    "updatedInput": { "command": "git push origin feature-branch" }
  },
  "suppressOutput": true
}

Top-level fields apply to any event: continue: false halts Claude entirely with a stopReason shown to the user, and systemMessage adds a warning. Event-specific fields sit under hookSpecificOutput: permissionDecision and updatedInput for PreToolUse, additionalContext for prompt submission and post-tool events, and for Stop a top-level decision: "block" with a reason that tells Claude why it must keep working.

Patterns

  • Formatting and linting after every edit with PostToolUse, so the model never has to be reminded.
  • Guarding destructive commands with PreToolUse scripts that exit 2.
  • Context injection at SessionStart (current branch, open tickets) or UserPromptSubmit (inject a standards reminder for every prompt).
  • Completion gates with Stop hooks that block until tests pass, always with a way out.
  • Audit logging with PostToolUse or SessionEnd writing to a file.
  • Protecting against compaction loss with PreCompact writing the transcript elsewhere.

Hook scripts should be fast, idempotent and quiet on success. Quote every variable derived from hook input, because tool arguments are model-generated and may contain shell metacharacters.

Exam focus

  • PreToolUse, UserPromptSubmit, Stop and SubagentStop can block; PostToolUse, Notification, SessionStart, SessionEnd and PreCompact cannot.
  • Exit code 2 blocks and returns stderr to Claude; other non-zero codes are non-blocking and show stderr to the user.
  • stdout of a successful UserPromptSubmit or SessionStart hook is added to context; for other events it is only shown to the user.
  • permissionDecision (allow/deny/ask) and updatedInput are PreToolUse outputs under hookSpecificOutput; decision: "block" with reason keeps a Stop from completing.
  • Matchers are regexes on tool names; empty or * matches all tools.
  • Hooks run arbitrary code: they are reviewed via /hooks when changed externally, and inputs must be quoted.

Check yourself

5 questions

  1. 1.Which exit code from a PreToolUse command hook blocks the tool call and feeds the hook's stderr back to Claude as the reason?

  2. 2.A team wants a formatter to run automatically every time Claude edits or writes a file. Which hook configuration is appropriate?

  3. 3.A PostToolUse hook exits with code 2 to block a dangerous Bash command, but the command has already executed. Why did blocking fail?

  4. 4.A compliance team wants hooks that developers cannot disable and plans to rely on them as the hard security gate for tool calls. How should an architect assess this plan?

  5. 5.A team wants to inject up-to-date context (for example the current sprint goal) whenever the user submits a prompt. Which hook event is appropriate, and how does the output reach Claude?