> ## Documentation Index
> Fetch the complete documentation index at: https://felimet-hub.jmcores.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 04-6 Hooks: What They Are, Use Cases, and Every Trigger Point

> Hooks turn 'remember to do this every time' from a soft model constraint into a hard system guarantee. This unit covers the 30 lifecycle events, settings.json configuration, exit code semantics, five handler types, and the supply-chain risks that come with executable hook code.

export const LearnerPrimer = ({items = [], lang = "zh"}) => {
  const t = lang === "en" ? {
    title: "Before this unit, be honest with yourself",
    sub: "If you can't answer these, that gap is exactly what this unit closes."
  } : {
    title: "讀這個單元前，先誠實面對",
    sub: "這幾題答不出來，正是這個單元要替你補的洞。"
  };
  const css = `
  .lp-root{--lp-bg:#FAF7F1;--lp-surface:rgba(191,117,81,0.05);--lp-border:rgba(0,0,0,0.09);--lp-edge:rgba(191,117,81,0.42);--lp-text:#2b2722;--lp-dim:#6f6a62;--lp-accent:#bf7551;border:1px solid var(--lp-border);border-left:3px solid var(--lp-edge);border-radius:13px;background:var(--lp-bg);color:var(--lp-text);overflow:hidden;margin:1.25rem 0;}
  .dark .lp-root{--lp-bg:#1b1a18;--lp-surface:rgba(207,138,104,0.07);--lp-border:rgba(255,255,255,0.08);--lp-edge:rgba(207,138,104,0.5);--lp-text:#e7e3da;--lp-dim:#a8a299;--lp-accent:#cf8a68;}
  .lp-head{display:flex;align-items:center;gap:9px;padding:13px 18px 11px;border-bottom:1px solid var(--lp-border);background:var(--lp-surface);}
  .lp-ic{color:var(--lp-accent);flex-shrink:0;}
  .lp-htx{display:flex;flex-direction:column;gap:1px;min-width:0;}
  .lp-title{font-size:14px;font-weight:650;line-height:1.3;letter-spacing:.01em;}
  .lp-sub{font-size:12px;color:var(--lp-dim);line-height:1.4;}
  .lp-list{list-style:none;margin:0;padding:10px 18px 14px;display:flex;flex-direction:column;gap:0;}
  .lp-item{display:flex;align-items:baseline;gap:11px;padding:7px 0;font-size:14px;line-height:1.6;border-top:1px solid var(--lp-border);}
  .lp-item:first-child{border-top:none;}
  .lp-mark{flex-shrink:0;color:var(--lp-accent);font-size:13px;font-weight:700;line-height:1.55;font-variant-numeric:tabular-nums;opacity:.85;}
  .lp-q{flex:1 1 0;min-width:0;color:var(--lp-text);}
  @media (max-width:620px){.lp-head{padding:12px 14px 10px;}.lp-list{padding:8px 14px 12px;}}
  `;
  return <div className="lp-root">
      <style>{css}</style>
      <div className="lp-head">
        <svg className="lp-ic" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10" /><circle cx="12" cy="12" r="6" /><circle cx="12" cy="12" r="2" /></svg>
        <span className="lp-htx">
          <span className="lp-title">{t.title}</span>
          <span className="lp-sub">{t.sub}</span>
        </span>
      </div>
      <ul className="lp-list">
        {items.map((q, i) => <li className="lp-item" key={i}>
            <span className="lp-mark">{String(i + 1).padStart(2, "0")}</span>
            <span className="lp-q">{q}</span>
          </li>)}
      </ul>
    </div>;
};

<LearnerPrimer
  lang="en"
  items={[
"Hooks are shell-level determinism, filling the gap left by 'the model might not remember'.",
"exit 1 does not block a tool call. Use exit 2 if you want to block.",
"PostToolUse without a timeout lets tasks pile up when files are saved rapidly.",
"You pasted a hook from a public repo without scanning it first. Unknown code is now running.",
"exit 2 with no reason: Claude only receives the raw stderr text.",
"A hook is executable code. Installing an unfamiliar hook is handing over shell access.",
"Writing 'things you want the model to do' as hooks runs them at the shell layer for nothing.",
]}
/>

<Info>
  **What this unit solves**

  Hooks are deterministic scripts that run automatically at 30 lifecycle event points around tool execution, session boundaries, and prompt submission: formatting, validation, interception, safety nets. They shift "things that must happen every time" from soft model constraints to hard system guarantees. Since Claude Code v2.1, the hook system has expanded substantially: 30 events, five handler types (command / http / mcp\_tool / prompt / agent), and exit code 2 blocking semantics. This unit covers the major trigger-point semantics, `settings.json` configuration, practical templates, and the supply-chain risks that come with hooks as executable code.
</Info>

## Learning objectives

* [ ] Name the 30 Claude Code hook events grouped by trigger timing, and state the semantics and typical use cases for PreToolUse, PostToolUse, Stop, and SessionStart.
* [ ] Write a correct `hooks` block in `settings.json` including `matcher` and `command`, and distinguish the three `matcher` parsing rules.
* [ ] Use `exit 2` to block a PreToolUse tool call, and use `additionalContext` to inject extra context for Claude.
* [ ] Identify the supply-chain risks of hooks as external executable code and perform a basic scan before introducing a third-party hook.
* [ ] Wire this project's `normalize_punctuation.py` and `validate_kb --hook` into a deterministic safety net using hooks.

***

## 1. What a hook is: deterministic automation that fills the "might not remember" gap

`CLAUDE.md` and rules are "please follow this" (soft constraints, context layer). A hook is "the system enforces this" (hard guarantee, shell layer). It **runs outside the model's reasoning chain**: the hook runs on your shell, reads the tool-input JSON from stdin, and decides whether to allow the tool call. The model does not know the hook is running by default. The exception is when you explicitly inform it via `additionalContext`.

Three key facts:

* **Hooks are deterministic**: they do not rely on the model "remembering" or "judging." If the shell says exit 2, it exits 2. Every time.
* **Hooks are executable code**: installing a hook is equivalent to adding a shell command to your workflow. That is a supply-chain entry point.
* **There are far more trigger points than most people realize**: as of 2026-06, there are 30 events grouped into four timing categories \[1].

<Tip>
  **Division of responsibility with 04-1 / 04-2 / 04-4**

  Writing "do not commit `.env`" in `CLAUDE.md` means the model will usually remember, but occasionally miss.
  Writing it in `.claude/rules/security.md` (path-scoped) means Claude receives the constraint when it reads relevant files, but it is still only context.
  Writing it as a hook (PreToolUse on Bash, matcher `git commit *`) means the shell checks staged files for `.env` and exits 2 to block it, **every single time, without exception**.

  There is only one criterion: **"should the model remember this, or should the system guarantee it?"** If the answer is the latter, use a hook.
</Tip>

## 2. The 30 trigger events grouped

Since Claude Code v2.1, the hook system covers 30 events (\[1]), grouped into four timing categories.

### 2.1 Once per session

| Event          | When it fires                                  | Typical uses                                                                    |
| -------------- | ---------------------------------------------- | ------------------------------------------------------------------------------- |
| `SessionStart` | Session start, resume, clear, or after compact | Inject dynamic context, load env vars, verify preconditions, set `sessionTitle` |
| `SessionEnd`   | Session end                                    | Clean up resources, archive transcript                                          |
| `Setup`        | Initial setup phase                            | One-time initialization (unlike SessionStart, which fires every time)           |

### 2.2 Once per conversation turn

| Event                 | When it fires                                               | Typical uses                                                                 |
| --------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `UserPromptSubmit`    | The moment the user submits a prompt                        | Inject prefix context (env state, reminders), reject and clear the prompt    |
| `UserPromptExpansion` | Prompt expansion phase                                      | Block prompt expansion                                                       |
| `Stop`                | Main agent completes, or `StopFailure` on failed completion | Wrap-up validation (run tests, static checks), send completion notifications |
| `PreCompact`          | Before compact                                              | Block compact, write state that must be preserved to transcript              |
| `PostCompact`         | After compact                                               | Re-attach required context                                                   |

### 2.3 Once per tool call (inside the agentic loop)

| Event                | When it fires                                      | Typical uses                                                                               |
| -------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `PreToolUse`         | Before a tool executes                             | Validate parameters, block dangerous commands, rewrite tool calls via `permissionDecision` |
| `PostToolUse`        | After a tool succeeds                              | Auto-format, lint, type check, log                                                         |
| `PostToolUseFailure` | After a tool fails                                 | Failure notification, attach debugging context                                             |
| `PostToolBatch`      | After a batch of tools, before the next model call | Batch-level interception, stop the agentic loop                                            |
| `PermissionRequest`  | When a permission prompt is triggered              | Automatic allow/deny decision                                                              |
| `PermissionDenied`   | When the user denies a request                     | Retry or fallback                                                                          |

### 2.4 Subagent / task events

| Event           | When it fires               | Typical uses                                                                                            |
| --------------- | --------------------------- | ------------------------------------------------------------------------------------------------------- |
| `SubagentStart` | Subagent starts             | Inject subagent-specific context, log                                                                   |
| `SubagentStop`  | Subagent ends               | Wrap-up validation (a `Stop` hook in Subagent frontmatter is automatically converted to `SubagentStop`) |
| `TeammateIdle`  | Agent team member goes idle | Orchestration logic, message routing                                                                    |
| `TaskCreated`   | A new task is created       | Log, apply default settings                                                                             |
| `TaskCompleted` | A task completes            | Notify, clean up                                                                                        |

### 2.5 Async / side-channel

| Event                               | When it fires                                                        |
| ----------------------------------- | -------------------------------------------------------------------- |
| `Notification`                      | When a notification requires user attention                          |
| `MessageDisplay`                    | Before a message is displayed (hook can rewrite the display content) |
| `ConfigChange`                      | When settings change                                                 |
| `CwdChanged`                        | When the working directory changes                                   |
| `FileChanged`                       | When a file changes externally                                       |
| `WorktreeCreate` / `WorktreeRemove` | When a git worktree is created / removed                             |
| `InstructionsLoaded`                | When CLAUDE.md / rules finish loading                                |
| `Elicitation` / `ElicitationResult` | When user input is needed                                            |

<Warning>
  **This unit focuses on the four high-frequency events**

  Covering all 30 events would dilute the focus. In practice, 80% of hooks use PreToolUse, PostToolUse, Stop, and SessionStart. Consult the documentation \[1] for the others when a specific need arises.
</Warning>

## 3. Configuration: the `settings.json` structure

Hook configuration lives in the `hooks` block of `settings.json`, three levels deep: **event, matcher group, hook handlers** (\[1]).

```json theme={null}
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(rm *)",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh",
            "args": []
          }
        ]
      }
    ]
  }
}
```

### 3.1 Five handler types

The `type` field determines how the handler runs (\[1]):

| Type       | How it runs                                        | Typical uses                                     |
| ---------- | -------------------------------------------------- | ------------------------------------------------ |
| `command`  | Shell script, stdin/stdout                         | 90% of cases: format validation, logging, lint   |
| `http`     | POST to a URL with the hook input JSON as the body | External system integration (CI, Slack webhooks) |
| `mcp_tool` | Calls a connected MCP tool                         | Reuse MCP capabilities across hooks              |
| `prompt`   | Single-turn LLM evaluation                         | LLM judgment needed without running a full agent |
| `agent`    | Runs a subagent with tools (**experimental**)      | LLM evaluation that requires tools               |

The default `type` is `command`. Omitting `type` is equivalent to explicitly specifying `command`.

### 3.2 Configuration locations and scope

| Path                          | Scope                                                                        |
| ----------------------------- | ---------------------------------------------------------------------------- |
| `~/.claude/settings.json`     | All projects (personal)                                                      |
| `.claude/settings.json`       | Current project (committed to version control)                               |
| `.claude/settings.local.json` | Current project (added to `.gitignore`)                                      |
| Managed policy                | Organization-wide, all machines (`allowManagedHooksOnly` blocks other hooks) |
| Plugin `hooks/hooks.json`     | Active while the plugin is enabled                                           |
| Skill / Subagent frontmatter  | Active for the lifetime of that Skill / Subagent                             |

<Warning>
  **Managed level takes precedence**

  Organizations can set `allowManagedHooksOnly: true` in a managed setting to block user-level, project-level, and plugin-level hooks. Plugins force-enabled via `enabledPlugins` are not affected \[1].
</Warning>

### 3.3 Matcher syntax

The `matcher` parsing rule is chosen automatically based on the characters present (\[1]):

| Pattern                         | Parsed as                | Example                        |
| ------------------------------- | ------------------------ | ------------------------------ |
| `"*"`, `""`, omitted            | Match all                | Always fires                   |
| Only letters, digits, `_`, `\|` | Exact match or `\|` list | `Bash`, `Edit\|Write`          |
| Any other characters            | JavaScript regex         | `^Notebook`, `mcp__memory__.*` |

**Each event matches a different field**:

* Tool events (PreToolUse / PostToolUse, etc.) match `tool_name`
* `SessionStart` matches the source (`startup` / `resume` / `clear` / `compact`)
* `Notification` matches the notification type
* `FileChanged` is a literal filename (not regex, not an exact-match list)
* Events with no matcher mechanism (UserPromptSubmit, PostToolBatch, Stop, TeammateIdle, TaskCreated, TaskCompleted, WorktreeCreate, WorktreeRemove, MessageDisplay, CwdChanged): always fire, or always ignored if a matcher is set

<Warning>
  **MCP tool matchers must include `.*`**

  `mcp__memory__.*` matches all memory tools. `mcp__memory` is treated as an exact string and matches nothing \[1].
</Warning>

### 3.4 Second-level filtering: `if`

The `if` field uses permission-rule syntax to provide finer-grained condition filtering (\[1]):

```json theme={null}
{
  "matcher": "Bash",
  "hooks": [
    {
      "type": "command",
      "if": "Bash(git commit *)",
      "command": "..."
    }
  ]
}
```

Supported rule formats include `"Bash(rm *)"` and `"Edit(*.ts)"`. **`&&` / `||` / list combinations are not supported.** Split into multiple handlers for multi-condition logic.

<Warning>
  **`if` only applies to tool events**

  Only PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, and PermissionDenied evaluate `if`. For all other events, a handler with `if` will never run \[1].
</Warning>

## 4. Input: the JSON on stdin

All `command` hooks receive a consistently structured JSON object from stdin (\[1]); `http` hooks receive it as the POST body.

Base shape:

```json theme={null}
{
  "session_id": "abc123",
  "transcript_path": "/home/user/.claude/projects/.../transcript.jsonl",
  "cwd": "/home/user/my-project",
  "permission_mode": "default",
  "effort": { "level": "high" },
  "hook_event_name": "PreToolUse"
}
```

Tool events also include:

```json theme={null}
{
  "tool_name": "Bash",
  "tool_input": { "command": "npm test" }
}
```

When running inside `--agent` or a Subagent, `agent_id` and `agent_type` are added. **Only `SessionStart` receives the `model` field** \[1].

<Tip>
  **Extracting fields from stdin**

  Bash example using `jq`:

  ```bash theme={null}
  FILE=$(jq -r '.tool_input.file_path // empty')
  if [ -z "$FILE" ]; then
    exit 0
  fi
  prettier --write "$FILE"
  ```
</Tip>

### 4.1 Path placeholders

Use the `args` exec form to avoid quoting issues (\[1]):

* `${CLAUDE_PROJECT_DIR}`: project root
* `${CLAUDE_PLUGIN_ROOT}`: plugin installation directory
* `${CLAUDE_PLUGIN_DATA}`: plugin persistent data
* `${user_config.*}`: plugin user settings

## 5. Exit codes and decision-making

`command` hooks signal their intent through exit codes (\[1]):

| Exit  | Meaning            | Effect                                                                                                                                |
| ----- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
| **0** | Success            | stdout is parsed as JSON; for `UserPromptSubmit` / `UserPromptExpansion` / `SessionStart`, plain stdout is added to Claude as context |
| **2** | Blocking error     | stderr is fed back to Claude; the action is blocked (exact effect depends on the event)                                               |
| Other | Non-blocking error | Transcript shows `<hook name> hook error` + first line of stderr; **execution continues**                                             |

### 5.1 What `exit 2` actually blocks

Not all events treat `exit 2` as a block. Events that truly block (\[1]):

* `PreToolUse` → blocks the tool call
* `UserPromptSubmit` → rejects and clears the prompt
* `UserPromptExpansion` → blocks expansion
* `Stop` / `SubagentStop` / `TeammateIdle` → prevents stopping
* `PreCompact` → blocks compact
* `PostToolBatch` → stops the agentic loop before the next model call
* `WorktreeCreate` → any non-zero exit fails the creation

**`exit 2` has no blocking effect on `PostToolUse` / `PostToolUseFailure` / `Notification`**, and only shows stderr. Unix intuition says "exit 1 is an error and should block," but Claude Code's hook system treats `exit 1` as a non-blocking error. To actually block, use `exit 2`.

### 5.2 Injecting additional context for Claude

Return JSON with `hookSpecificOutput.additionalContext` to make Claude aware of something (the corresponding `hookEventName` is required) (\[1]):

```json theme={null}
{
  "hookSpecificOutput": {
    "hookEventName": "PostToolUse",
    "additionalContext": "This file is generated. Edit src/schema.ts and run `bun generate` instead."
  }
}
```

Where the context is injected depends on the event:

* `SessionStart` / `Setup` / `SubagentStart` → beginning of the conversation
* `UserPromptSubmit` / `UserPromptExpansion` → attached to the prompt
* `PreToolUse` / `PostToolUse*` / `PostToolBatch` → attached to the tool result

**10,000-character limit.** For longer values, write to a file and attach a preview. Values from multiple hooks are all injected. **Use declarative statements, not imperative commands** (to avoid triggering prompt injection defenses) \[1].

### 5.3 General JSON output fields

| Field                       | Purpose                                                                      |
| --------------------------- | ---------------------------------------------------------------------------- |
| `continue` (default `true`) | Set to `false` to stop Claude entirely                                       |
| `stopReason`                | Message shown to the user when Claude stops                                  |
| `suppressOutput`            | Hides stdout from the transcript                                             |
| `systemMessage`             | Warning shown to the user                                                    |
| `terminalSequence`          | Emit allowlisted OSC / BEL escapes (replaces writing directly to `/dev/tty`) |

### 5.4 Decision control modes (different events use different fields)

| Event group                                                              | Mode                 | Key fields                                                                              |
| ------------------------------------------------------------------------ | -------------------- | --------------------------------------------------------------------------------------- |
| `UserPromptSubmit`, `PostToolUse*`, `Stop`, `PreCompact`, `ConfigChange` | Top-level `decision` | `decision: "block"`, `reason`                                                           |
| `PreToolUse`                                                             | `hookSpecificOutput` | `permissionDecision: allow/deny/ask/defer`, `permissionDecisionReason`                  |
| `PermissionRequest`                                                      | `hookSpecificOutput` | `decision.behavior: allow/deny`, `updatedInput`, permission rules                       |
| `PermissionDenied`                                                       | `hookSpecificOutput` | `retry: true`                                                                           |
| `WorktreeCreate`                                                         | stdout path          | or `hookSpecificOutput.worktreePath`                                                    |
| `Elicitation` / `ElicitationResult`                                      | `hookSpecificOutput` | `action: accept/decline/cancel`, `content`                                              |
| `MessageDisplay`                                                         | `hookSpecificOutput` | `displayContent` rewrites what appears on screen                                        |
| `SessionStart`                                                           | `hookSpecificOutput` | `additionalContext`, `initialUserMessage`, `watchPaths`, `sessionTitle`, `reloadSkills` |

## 6. Five practical templates

### 6.1 PreToolUse: blocking a dangerous Bash command

```bash theme={null}
#!/bin/bash
# .claude/hooks/block-rm.sh
COMMAND=$(jq -r '.tool_input.command')
if echo "$COMMAND" | grep -q 'rm -rf'; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "Destructive command blocked by hook"
    }
  }'
else
  exit 0
fi
```

Corresponding `settings.json`:

```json theme={null}
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh"
          }
        ]
      }
    ]
  }
}
```

<Tip>
  **Why `permissionDecision: deny` instead of `exit 2`**

  For `PreToolUse`, **returning JSON with `permissionDecision` is cleaner than `exit 2`**: you can attach `permissionDecisionReason` for Claude to read, and you can choose between `deny` (explicit refusal), `allow` (explicitly bypass the prompt), `ask` (return to user confirmation), or `defer` (fall through to normal flow). `exit 2` can only communicate via stderr and cannot express `allow` \[1].
</Tip>

### 6.2 PostToolUse: auto-formatting (this project)

```json theme={null}
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "python ${CLAUDE_PROJECT_DIR}/.claude/scripts/normalize_punctuation.py"
          }
        ]
      }
    ]
  }
}
```

<Note>
  **Read the path from stdin, not from environment variables**

  The script above scans for files to process on its own. To target "the file just edited," parse `.tool_input.file_path` from the hook input JSON on stdin (see the `jq` example in Section 4). Claude Code does **not** provide path-carrying environment variables like `$CLAUDE_TOOL_INPUT_FILE_PATH` (as of 2026-06). The only variables available for expansion inside `command` strings are addressing variables such as `${CLAUDE_PROJECT_DIR}`, `${CLAUDE_PLUGIN_ROOT}`, and `${CLAUDE_PLUGIN_DATA}` \[1].
</Note>

<Warning>
  **PostToolUse must have a timeout and run incrementally**

  Without an explicit timeout, PostToolUse defaults to a 600-second limit \[1]. With rapid sequential edits, multiple hooks stack up and consume resources. This project's `normalize_punctuation.py` is lightweight on a single file, but for heavy hooks (tsc, ruff --fix) you must:

  * Set a `timeout` field (30-60 seconds) to prevent hangs
  * Use incremental flags like `--incremental`
  * On Windows, wrap the timeout in PowerShell (`Start-Process -TimeoutSec`) if you depend on GNU `timeout`
</Warning>

### 6.3 Stop: running wrap-up validation (this project)

```json theme={null}
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "python ${CLAUDE_PROJECT_DIR}/.claude/scripts/validate_kb.py --hook"
          }
        ]
      }
    ]
  }
}
```

`validate_kb.py --hook` runs in Stop hook mode: **it blocks only on CRITICAL/HIGH findings, and fails open on tool errors so it does not disrupt normal flows**. It serves as a deterministic safety net in this project: **even if the natural-language-driven self-check is skipped, the hook will block at the end of the turn**.

### 6.4 SessionStart: injecting environment variables

A SessionStart hook can persist environment variables via the `$CLAUDE_ENV_FILE` environment variable (which points to a session-specific temporary file) \[1]:

```bash theme={null}
if [ -n "$CLAUDE_ENV_FILE" ]; then
  echo 'export NODE_ENV=production' >> "$CLAUDE_ENV_FILE"
  echo 'export CUSTOM_FLAG=1' >> "$CLAUDE_ENV_FILE"
fi
exit 0
```

This is the **officially supported way** to set environment variables in Claude Code (as of 2026-06, only `SessionStart` / `Setup` / `CwdChanged` / `FileChanged` support this).

### 6.5 Tool call logging (PostToolUse)

```bash theme={null}
#!/bin/bash
# .claude/hooks/log-tool.sh
jq -c '{
  ts: now | todate,
  session_id: .session_id,
  tool: .tool_name,
  input: .tool_input
}' >> "${CLAUDE_PROJECT_DIR}/.claude/logs/tools.jsonl"
exit 0
```

The minimum fields satisfy the audit requirements described in [01-4 Context Engineering](/en/code-agent/foundations/context-engineering): timestamp, session\_id, tool, input. Entries can be replayed, audited, or fed to [04-4 Skills](/en/code-agent/customization/skills) for pattern analysis.

<Tip>
  **Background execution**

  Add `"async": true` to prevent the hook from blocking the main flow (\[1]):

  ```json theme={null}
  { "type": "command", "command": "long-task.sh", "async": true }
  ```

  Pair with `"asyncRewake": true` to wake Claude via a system reminder when the async hook exits 2.
</Tip>

## 7. Hooks inside Skill / Subagent frontmatter

Hooks are not limited to `settings.json`. **Skills and Subagents can declare hooks in their frontmatter** and those hooks are active only for the lifetime of that Skill / Subagent (\[1]):

```yaml theme={null}
---
name: secure-operations
description: Perform operations with security checks
hooks:
  PreToolUse:
    - matcher: "Bash"
      hooks:
        - type: command
          command: "./scripts/security-check.sh"
---
```

<Tip>
  **A Subagent's `Stop` hook is automatically converted to `SubagentStop`**

  A `Stop` hook declared in Subagent frontmatter is **automatically converted to `SubagentStop`**, no manual change required \[1]. This makes "run validation when the subagent finishes" a one-liner inside the frontmatter.
</Tip>

## 8. Advanced: HTTP / MCP / prompt handlers

### 8.1 HTTP handler template

```json theme={null}
{
  "type": "http",
  "url": "http://localhost:8080/hooks/pre-tool-use",
  "timeout": 30,
  "headers": { "Authorization": "Bearer $MY_TOKEN" },
  "allowedEnvVars": ["MY_TOKEN"]
}
```

`allowedEnvVars` is an allowlist: only the listed environment variables are exposed to the hook. **Do not write values directly into `headers`** (they end up in the plugin config file). Reference them as `$VAR` instead \[1].

### 8.2 MCP tool handler template

```json theme={null}
{
  "type": "mcp_tool",
  "server": "my_server",
  "tool": "security_scan",
  "input": { "file_path": "${tool_input.file_path}" }
}
```

Useful for reusing MCP capabilities across hooks, for example running a security scan via MCP after every file edit.

### 8.3 Prompt handler template

```json theme={null}
{
  "type": "prompt",
  "prompt": "Review the tool call below and decide if it's safe. Respond with `allow` or `deny`."
}
```

Use when LLM judgment is needed but you do not want to run a full agent. Default timeout is 30 seconds \[1].

## 9. Security: hooks are executable code; external sources are supply chain

The `command` field of a hook is **executed directly by the shell**. Installing an unvetted hook is equivalent to handing execution rights over a piece of third-party code.

### 9.1 Known threats

* **CVE-2025-59536** (CVSS 8.7): code inside `.claude/` executes before the user approves the trust dialog. Fixed in Claude Code v1.0.111+ \[2]. Unpatched versions are vulnerable to pre-planted malicious commands.
* **CVE-2026-21852**: an attacker-controlled app overwrites `ANTHROPIC_BASE_URL`, redirecting API traffic and exfiltrating the API key before trust is granted. Fixed in v2.0.65+ \[2].
* **Snyk ToxicSkills (2026-02)**: 36% of public skills contained prompt injection. Hooks and skills share the same supply-chain surface. Scan before installing.

### 9.2 Required scan before importing any external hook

```bash theme={null}
# 1. Detect suspicious outbound commands inside hooks/skills
rg -n 'curl|wget|nc|ssh|ANTHROPIC_BASE_URL|enableAllProjectMcpServers' .claude/

# 2. Detect hidden Unicode control characters (zero-width, bidi overrides)
rg -nP '[\x{200B}-\x{200D}\x{202A}-\x{202E}]' .claude/

# 3. Detect HTML comments, script tags, base64 blobs
rg -n '<!--|<script|data:text/html|base64,' .claude/

# 4. Detect suspicious permissions or outbound references
rg -n 'curl|wget|nc|scp|ssh|enableAllProjectMcpServers|ANTHROPIC_BASE_URL' .claude/
```

Any hit requires a manual review of the surrounding context before installation.

### 9.3 Principle of least privilege

A hook's `command` should be granted only the access required to do its job:

* **Do not read `~/.ssh`, `~/.aws`, or `**/.env*` inside a hook.** These paths belong in `permissions.deny`.
* **Do not let hooks write outside the repo.** Use `${CLAUDE_PROJECT_DIR}` to stay inside the project.
* **Use `allowedEnvVars` to allowlist environment variables** (HTTP handlers) rather than exposing the entire process environment.

### 9.4 Versioning and managed policy

* `disableAllHooks: true` in settings temporarily disables all hooks (**managed-level hooks can only be disabled by a managed-level setting**) \[1].
* Organizations can set `allowManagedHooksOnly: true` to block user-level, project-level, and plugin-level hooks (except those force-enabled via `enabledPlugins`).
* Keep Claude Code >= v2.0.65 (CVE-2026-21852 fix) and >= v2.1.139 (hooks use `systemMessage` / `terminalSequence` instead of writing directly to `/dev/tty`) \[1, 2].

## 10. Tool comparison

Hook mechanism support varies widely across tools (as of 2026-06):

| Concept                 | Anthropic Claude (primary)                                                                                | OpenAI Codex                                        | Google Antigravity   | GitHub Copilot CLI      | Cursor (brief)             |
| ----------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | -------------------- | ----------------------- | -------------------------- |
| Pre-tool hook           | PreToolUse in `settings.json`                                                                             | Codex hooks (`config.toml`)                         | Antigravity workflow | Copilot hooks (preview) | (no first-class mechanism) |
| Post-tool hook          | PostToolUse in `settings.json`                                                                            | Configurable                                        | Configurable         | Configurable            | N/A                        |
| Session lifecycle hooks | SessionStart / SessionEnd / Stop                                                                          | Configurable                                        | Configurable         | Configurable            | N/A                        |
| Event count             | 30 lifecycle events \[1]                                                                                  | Configurable                                        | Configurable         | Configurable            | N/A                        |
| Handler types           | command / http / mcp\_tool / prompt / agent                                                               | command primarily                                   | command primarily    | command primarily       | N/A                        |
| Matcher syntax          | Exact / `\|` list / regex three modes                                                                     | Configurable                                        | Configurable         | Configurable            | N/A                        |
| Blocking mechanism      | `exit 2` + `permissionDecision: deny`                                                                     | Configurable                                        | Configurable         | Configurable            | N/A                        |
| Context injection       | `additionalContext` (10k limit)                                                                           | N/A                                                 | N/A                  | N/A                     | N/A                        |
| Config locations        | `~/.claude/settings.json`, `.claude/settings.json`, plugin `hooks/hooks.json`, Skill/Subagent frontmatter | `~/.codex/config.toml`, `<repo>/.codex/config.toml` | Configurable         | Configurable            | N/A                        |
| Determinism guarantee   | Hard guarantee (`exit 2`)                                                                                 | Soft guarantee                                      | Soft guarantee       | Soft guarantee          | N/A                        |

<Note>
  **Name clarifications and scope**

  * **Claude Code's hook system as of 2026-06 is a full 30-event lifecycle system** (\[1]). Other CLI tools' "hooks" mostly refer to command hooks only, with far fewer events.
  * **OpenAI Codex, Antigravity, GitHub Copilot CLI** hook mechanisms are subject to each vendor's current documentation. Entries marked "source pending" above lack authoritative citations.
  * **Cursor** as of 2026-06 does not provide a first-class hook mechanism in its CLI or IDE.
  * This project's `validate_kb --hook` (Stop mode) is designed against the primary reference (Claude Code) semantics in this table.
</Note>

## 11. Hands-on exercises

<Note>
  **30-minute exercise**

  1. **PostToolUse auto-formatting**: following this project's `PostToolUse` template, wire `normalize_punctuation.py` into `.claude/settings.json` and edit any `.md` to observe automatic normalization.
  2. **PreToolUse blocking dangerous Bash**: write `block-rm.sh` with the corresponding settings. Try to have Claude run `rm -rf /tmp/xxx` and observe the `permissionDecision: deny` response and the reason Claude receives.
  3. **Stop wrap-up validation**: wire in `validate_kb.py --hook`, deliberately create a unit with a frontmatter missing an `updated` field, and observe the Stop hook blocking and reporting it.
  4. **Supply-chain scan**: run the `rg` scans from Section 9 against `~/.claude/` and `.claude/` and confirm there are no suspicious outbound references and no hidden Unicode.
  5. **SessionStart env injection**: write a SessionStart hook that sets `MY_FLAG=1` via `CLAUDE_ENV_FILE`, then run `echo $MY_FLAG` in a new session to verify it takes effect.
</Note>

## 12. Common pitfalls

<Warning>
  **Anti-pattern list**

  * **PostToolUse hook without a timeout**: each file save triggers a long task; rapid successive saves cause tasks to stack up and consume resources. Every hook should have a `timeout` and a reasonable ceiling.
  * **Binding expensive full type-checking to every Write/Edit without `--incremental`**: this slows iteration. tsc, mypy, and ruff all have incremental modes. Use them.
  * **Copying a hook config from a public repo and pasting it directly without scanning the `command` content**: supply-chain risk. Before pasting, run `rg` and read every `command`'s shell code.
  * **Using `exit 1` expecting it to block**: Unix intuition says "exit 1 is an error and should block." Claude Code's hook system disagrees: `exit 1` is a non-blocking error. Use `exit 2`, or return JSON with `permissionDecision: deny`, to actually block.
  * **Blocking with PreToolUse `exit 2` without giving Claude a reason**: `exit 2` feeds stderr back to Claude, but it is only plain text. For `PreToolUse`, returning JSON with `permissionDecision: deny` and `permissionDecisionReason` is more structured and easier for the model to understand and respect \[1].
  * **Writing "things you want the model to do" as hooks**: hooks run at the shell layer, outside the model's reasoning chain. Things you want the model to "remember" belong in `CLAUDE.md` or rules. Only things that need a "system-level guarantee" should go into hooks. Confusing the two results in hooks running context-maintenance logic at the shell layer, wasting resources for no benefit.
  * **Mixing `\|` list syntax with other regex characters in `matcher`**: `\|` only works in the exact-list syntax. Adding other characters (like `Bash|Edit\.ts`) causes the matcher to be parsed as a regex, which is probably not what you want \[1].
</Warning>

## Self-check

<Check>
  **The bar for passing this unit**

  1. Can you state the fundamental difference between a hook and `CLAUDE.md` / rules? (Shell determinism vs. context soft constraint)
  2. Can you write a `PreToolUse` hook that blocks `git commit` when `.env` is staged, in under 5 minutes?
  3. Can you state the semantics of `exit 0`, `exit 2`, and other exit codes across different events?
  4. Can you state the three supply-chain protection SOPs for hooks as executable code?
  5. In your current workflow, is there one thing that "should always happen but might be forgotten" that you could guarantee with a PostToolUse or Stop hook? Name the trigger event, the command to run, and the reasoning for choosing PostToolUse over Stop (or vice versa).
</Check>

## Sources and further reading

Factual claims are grounded in official documentation; fast-changing items are annotated as of 2026-05.

<div className="references">
  * \[1] Anthropic, "Hooks," code.claude.com, 2026. \[Online]. Available: [https://code.claude.com/docs/en/hooks](https://code.claude.com/docs/en/hooks) (as of 2026-06; covers 30 events, five handler types, three-mode matcher parsing, `exit 2` blocking semantics, `additionalContext` injection, `v2.1.139+` switch to `terminalSequence`)

  * \[2] Anthropic, "Claude Code release notes," code.claude.com, 2026. \[Online]. Available: [https://code.claude.com/docs/en/changelog](https://code.claude.com/docs/en/changelog) (as of 2026-06; CVE-2025-59536 and CVE-2026-21852 fix versions)

  * \[3] Anthropic, "Extend Claude with skills," code.claude.com, 2026. \[Online]. Available: [https://code.claude.com/docs/en/skills](https://code.claude.com/docs/en/skills) (as of 2026-06; `hooks` configuration inside Skill frontmatter)

  * \[4] Anthropic, "Create custom subagents," code.claude.com, 2026. \[Online]. Available: [https://code.claude.com/docs/en/subagents](https://code.claude.com/docs/en/subagents) (as of 2026-06; `hooks` inside Subagent frontmatter and automatic `Stop` to `SubagentStop` conversion)
</div>

* Configuration layer model: [02-1 The Configuration Layer Model](/en/code-agent/configuration/config-layer-model).
* Division of responsibility between `CLAUDE.md` soft constraints and hook hard guarantees: [04-1 CLAUDE.md and Memory Files](/en/code-agent/customization/claude-md-memory).
* The `rules` path-scoped mechanism: [04-2 Rules](/en/code-agent/customization/rules).
* Hooks inside Skill frontmatter: [04-4 Skills](/en/code-agent/customization/skills).
* Subagent lifecycle hooks: [04-5 Subagents](/en/code-agent/customization/subagents).
* Supply-chain risks: [03-3 Security, Privacy, and Supply-Chain Risk](/en/code-agent/judgment/security-privacy-supply-chain).
