跳到正文
DEV Community· Alex Cloudstar·· 3 小時前AI 評分58

Claude Code Hooks:33 個事件、設定格式與 5 個可用範例

Claude Code Hooks: Every Event, the Config Format and Five Working Examples

AI 導讀

Claude Code Hooks 是在 Claude Code 生命週期指定節點自動執行的用戶自訂處理程序,文章整理文件列出的 33 個事件及設定方式。文中説明退出碼與 JSON 輸出如何控制操作,並提供 5 個涵蓋通知、格式化、命令攔截、注入上下文及測試的範例。命令型 Hook 以用戶權限執行,除非自行建立隔離,否則沒有沙盒保護。

正文

Claude Code Hooks: Every Event, the Config Format and Five Working Examples

A line in CLAUDE.md that says "always run the formatter after editing" works most of the time. Most of the time is the problem. The model reads the instruction, weighs it against everything else in the context, and sometimes decides this edit is too small to bother.

Hooks fix that by taking the decision away from the model. This post covers what Claude Code hooks are, every event the docs list, how to configure them, how exit codes and JSON output control Claude, and five examples you can paste into a settings file. I checked the details against the official hooks reference and hooks guide on October 23, 2026. Hooks change often, so treat the docs as the source of truth if anything here has drifted.

What Are Claude Code Hooks?

Claude Code hooks are user-defined handlers that Claude Code runs automatically at specific points in its lifecycle, such as before a tool call, after a file edit, or when Claude finishes responding. Most hooks are shell commands. Because the harness runs them, not the model, they fire every time, which makes them deterministic.

That last part is the whole point. A skill or a CLAUDE.md rule is a suggestion the model may follow. A hook is code that runs whether the model remembers it or not. If you want a rule enforced, you write a hook. If you want a rule suggested, you write a skill.

If you are still sorting out how hooks sit next to skills, plugins and MCP servers, my plugin and skills marketplace guide has the short version, and this post is the long one.

Claude Code Hook Events and When They Fire

The hooks reference lists 33 events. Each one is a key you put inside the hooks object in a settings file. These are the events as the docs list them, in the order they appear there.

Event When it fires
SessionStart A session begins or resumes
Setup Started with --init-only, or with --init or --maintenance in -p mode
UserPromptSubmit A prompt is submitted, before Claude processes it
UserPromptExpansion A command you typed expands into a prompt, before it reaches Claude
PreToolUse Before a tool call runs. Can block
PermissionRequest A tool call needs a permission decision
PermissionDenied Auto mode denies a tool call
PostToolUse After a tool call succeeds
PostToolUseFailure After a tool call fails
PostToolBatch After a full batch of parallel tool calls resolves, before the next model call
Notification Claude Code sends a notification
MessageDisplay While assistant message text is displayed
SubagentStart A subagent is spawned
SubagentStop A subagent finishes
TaskCreated A task is being created with TaskCreate
TaskCompleted A task is being marked completed
Stop Claude finishes responding
StopFailure The turn ends because of an API error
TeammateIdle An agent team teammate is about to go idle
InstructionsLoaded A CLAUDE.md or .claude/rules/*.md file is loaded
ConfigChange A configuration file changes during a session
CwdChanged The working directory changes
DirectoryAdded A working directory is added mid-session with /add-dir
FileChanged A watched file changes on disk
WorktreeCreate A worktree is being created
WorktreeRemove A worktree created by a WorktreeCreate hook is removed
PreCompact Before context compaction
PostCompact After context compaction completes
PreModelSwitch Before a requested model switch is applied. Can block
PostModelSwitch After the session's model changes
Elicitation An MCP server requests user input during a tool call
ElicitationResult After a user responds to an MCP elicitation
SessionEnd A session terminates

Most people need PreToolUse, PostToolUse, Stop, SessionStart and Notification. The rest cover narrower jobs like auditing config changes or reacting to compaction.

A few details from the docs that save you debugging time. PreToolUse and PostToolUse fire on every tool call in the agentic loop. Stop fires whenever Claude finishes responding, not only when the task is truly done, and it does not fire on user interrupts. And a PostToolUse hook cannot undo anything, because the tool has already run.

Where Claude Code Hooks Are Configured

Hooks live in JSON. You can put them in several places, and the entries merge across levels instead of replacing each other.

Location Scope Shareable
~/.claude/settings.json All your projects No
.claude/settings.json Single project Yes, committed to the repo
.claude/settings.local.json Single project No, gitignored
Managed policy settings Organization-wide Yes, admin-controlled
Plugin hooks/hooks.json While the plugin is enabled Yes
Skill frontmatter The rest of the session once the skill is invoked Yes
Subagent frontmatter While that subagent runs Yes

I put personal conveniences like notifications in ~/.claude/settings.json, and team rules like a formatter in the committed .claude/settings.json.

You can browse what is active by typing /hooks at the Claude Code prompt. It opens a read-only browser of your configured hooks. If a new hook does not appear after a few seconds, restart the session.

The JSON shape

Three levels: an event, a matcher group, and one or more handlers.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-bash.sh"
          }
        ]
      }
    ]
  }
}

If your settings file already has a hooks key, add the new event as a sibling of the existing ones. Each event name is a key inside the single hooks object. Overwriting the whole object is the most common way to delete a hook you forgot you had.

Hook types

The type field decides what the handler does.

  • command runs a shell command and passes the event JSON on stdin.
  • http POSTs the event JSON to a URL.
  • mcp_tool calls a tool on a configured MCP server.
  • prompt sends your prompt and the event data to a Claude model for a single-turn yes or no decision.
  • agent spawns a subagent that can use tools to verify a condition. The docs mark this one experimental.

Shared fields include if, timeout, statusMessage and once. Command hooks add args (exec form, no shell), async, shell and onFailure. Timeouts default to 600 seconds for command, http and mcp_tool, 30 for prompt and 60 for agent, so set timeout on anything slow.

Matchers: Choosing Which Calls a Hook Sees

The matcher field filters when a group fires. How it is evaluated depends on what it contains.

Matcher value Evaluated as
"*", "", or omitted Match everything
Only letters, digits, _, -, spaces, commas and pipes Exact string, or a list separated by pipes or commas
Any other character JavaScript regular expression, unanchored

What the matcher is compared against depends on the event. For PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest and PermissionDenied, it is the tool name, like Bash, Edit or mcp__memory__.*. MCP tools are named mcp__<server>__<tool>, and matching every tool from one server needs the .*.

For SessionStart it is how the session started: startup, resume, clear, compact or fork. For Notification it is the notification type, such as permission_prompt or idle_prompt. UserPromptSubmit, Stop and several others take no matcher and always fire.

On tool events there is also an if field that uses permission-rule syntax, like "Bash(git *)" or "Edit(*.ts)". It holds one rule and no boolean operators. The docs call it best-effort and say to use the permission system when you need a hard allow or deny.

Exit Codes: How a Hook Blocks Claude

A command hook talks to Claude Code through its exit code, stdout and stderr.

  • Exit 0 means success. If stdout is a single JSON object, Claude Code reads its fields. Plain stdout is added to Claude's context only for UserPromptSubmit, UserPromptExpansion, SessionStart and PostModelSwitch. Claude never sees stderr on exit 0.
  • Exit 2 is a blocking error. On events that can block, the action stops and stderr becomes the reason. Exit 2 wins even if JSON on stdout says "allow".
  • Any other code is a non-blocking error. The action proceeds, and the first line of stderr shows in the notice.

Which events can actually be blocked matters. By the docs, exit 2 blocks on PreToolUse, UserPromptSubmit, UserPromptExpansion, Stop, SubagentStop, TeammateIdle, TaskCreated, TaskCompleted, ConfigChange (except policy settings), PostToolBatch, PreCompact, PreModelSwitch, Elicitation, ElicitationResult, WorktreeCreate and WorktreeRemove.

It cannot block PostToolUse, PostToolUseFailure, PermissionRequest (use a decision instead), Notification, SessionStart, SubagentStart, SessionEnd, PostCompact and the other observational events. On those, exit 2 shows stderr to you and execution continues.

One gotcha from the guide: exit 0 from a PreToolUse hook does not approve the call. The tool call still goes through the normal permission flow.

JSON output for finer control

Exit codes are blunt. If you need more, exit 0 and print one JSON object to stdout. Print only the JSON. Anything else on stdout, like output from a shell profile, breaks parsing. The docs say to pick one approach per hook: exit 2 with stderr, or exit 0 with JSON.

Universal fields are continue (set false to stop Claude entirely), stopReason, systemMessage, suppressOutput and terminalSequence. Event-specific decisions go in different places. PreToolUse uses hookSpecificOutput with permissionDecision set to allow, deny, ask or defer, plus permissionDecisionReason. PostToolUse and Stop use a top-level decision: "block" with a reason.

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Database writes are not allowed"
  }
}

A field at the wrong level is a common reason a hook silently does nothing: permissionDecision belongs inside hookSpecificOutput, not at the top.

What a hook receives and which variables it can use

Command hooks get JSON on stdin. Common fields include session_id, transcript_path, cwd, permission_mode and hook_event_name. Tool events add tool_name, tool_input and tool_use_id, which is how a script finds the file path or command Claude is about to use. SessionStart hooks also get a source, and UserPromptSubmit hooks get the prompt text.

Environment variables the docs list for hooks:

  • CLAUDE_PROJECT_DIR: the project root where the session started
  • CLAUDE_PLUGIN_ROOT and CLAUDE_PLUGIN_DATA: a plugin's install and data directories
  • CLAUDE_PLUGIN_OPTION_<KEY>: a plugin's user-config values
  • CLAUDE_ENV_FILE: a file a SessionStart or CwdChanged hook can write environment variables into, which Claude Code runs as a preamble before each Bash command

Use ${CLAUDE_PROJECT_DIR} in script paths so they work from any directory.

Five Claude Code Hook Examples

These are examples, adapted from the official hooks guide. Test each in a throwaway project before you commit it to a repo your team uses.

Example 1: Get a desktop notification when Claude needs input

The Notification event fires when Claude is waiting for input or permission. This is the guide's macOS version. Put it in ~/.claude/settings.json.

{
  "hooks": {
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

The empty matcher fires on every notification type. To narrow it, set the matcher to a type like permission_prompt or idle_prompt. The guide gives notify-send for Linux and a PowerShell variant for Windows. On macOS, if nothing appears, osascript routes through Script Editor, and the docs say to enable notifications for Script Editor in System Settings.

Example 2: Format every file Claude edits

This PostToolUse hook runs Prettier on the file Claude just wrote. It goes in .claude/settings.json in the project root.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ]
  }
}

jq pulls the path out of the stdin JSON and Prettier does the rest. It needs jq installed. One limit the guide calls out: Claude can also change files through shell commands, which an Edit|Write matcher never sees. If your hook must see every change, add a Stop hook that checks the working tree once per turn.

Example 3: Block dangerous Bash commands

This PreToolUse hook exits 2 when a command matches a pattern you never want Claude to run. The guide's own script blocks edits to protected files with the same mechanism. I adapted it to read tool_input.command for the Bash tool.

Save this as .claude/hooks/block-dangerous.sh:

#!/bin/bash
# block-dangerous.sh (example adapted from the hooks guide's protect-files.sh)

INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

BLOCKED_PATTERNS=("rm -rf" "git push --force" "DROP TABLE")

for pattern in "${BLOCKED_PATTERNS[@]}"; do
  if [[ "$COMMAND" == *"$pattern"* ]]; then
    echo "Blocked: command matches '$pattern'. Ask the user before running it." >&2
    exit 2
  fi
done

exit 0

Make it executable with chmod +x .claude/hooks/block-dangerous.sh, then register it:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-dangerous.sh"
          }
        ]
      }
    ]
  }
}

Claude Code blocks the call before it runs and passes your stderr message to Claude, so it can pick another approach. Substring matching is crude. A command like rm -rf with two spaces slips past it. Treat this as a tripwire for honest mistakes, not as a security boundary, and read the next section before you rely on it.

The docs add one useful fact: a PreToolUse hook that returns a deny blocks the tool even in bypassPermissions mode or with --dangerously-skip-permissions. That is why hooks pair well with the permission setup in my least-privilege guide.

Example 4: Inject context at SessionStart

Compaction summarizes a long conversation and can lose details. A SessionStart hook with the compact matcher puts critical context back. This is the guide's version, in .claude/settings.json.

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "compact",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Reminder: use Bun, not npm. Run bun test before committing. Current sprint: auth refactor.'"
          }
        ]
      }
    ]
  }
}

Plain text a SessionStart command writes to stdout goes into Claude's context. Swap the echo for something dynamic, like git log --oneline -5. The docs point out that if you want the same context on every session start, CLAUDE.md is the better home. Use the hook when the context is dynamic or only needed at a specific moment. Change the matcher to startup to inject on a fresh session instead.

Example 5: Run tests before Claude stops

A Stop hook can refuse to let Claude finish. This one runs the test suite and, if it fails, exits 2 with the failure output so Claude keeps working. It is an example I built from two pieces of the docs: the exit-2 behavior for Stop, and the guide's stop_hook_active check.

Save this as .claude/hooks/tests-on-stop.sh:

#!/bin/bash
# tests-on-stop.sh (example)

INPUT=$(cat)

# Stop hooks can re-trigger themselves. Bail out if we already blocked once.
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0
fi

cd "$CLAUDE_PROJECT_DIR" || exit 0

if ! OUTPUT=$(bun test 2>&1); then
  echo "Tests are failing. Fix them before finishing:" >&2
  echo "$OUTPUT" | tail -n 30 >&2
  exit 2
fi

exit 0

Swap bun test for your own test command. Then register it:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/tests-on-stop.sh",
            "timeout": 300
          }
        ]
      }
    ]
  }
}

The stop_hook_active guard matters. The docs say Claude Code overrides a Stop hook after it blocks eight times in a row with no tool call in between, and the cap can be raised with CLAUDE_CODE_STOP_HOOK_BLOCK_CAP. Because Stop fires after every response, a slow test suite will run more often than you expect. For a lighter touch, the guide shows a prompt hook on Stop that asks a model whether all requested tasks are done.

Security: Hooks Run With Your Permissions

A command hook is a shell command that runs as your user, with your environment and access to your files. There is no sandbox around it unless you build one. That has a few consequences.

Read every hook you did not write. A hook in a shared .claude/settings.json or a plugin's hooks/hooks.json runs on your machine once you trust the project or enable the plugin. Cloning a repo and accepting its settings means running its commands. The docs say project hooks in settings files and frontmatter are subject to workspace trust, and frontmatter hooks in project subagents run only after you accept the trust dialog. Treat that dialog the way you treat running an install script.

Quote your variables. The hook input contains file paths and commands the model chose. A path with a space or shell metacharacter in it will break an unquoted script or, worse, run something you did not intend. The docs recommend exec form with args for paths, and they note shell form needs careful quoting.

Do not mistake if or substring matching for enforcement. The if field is best-effort by the docs' own words. For hard rules, use permission rules and managed settings, and use hooks as one more layer.

For teams, managed settings add controls. allowManagedHooksOnly blocks user, project, local and plugin hooks, and allowedHttpHookUrls limits which URLs HTTP hooks can reach. I would also read the plugin security page in the docs before enabling any third-party marketplace, since plugin hooks run with the same authority as the ones you write yourself.

Hooks vs Skills vs Permissions vs CLAUDE.md

These four get confused because they all "tell Claude what to do". They differ in who enforces the rule.

Hooks Skills Permissions CLAUDE.md
What it is Handlers that run at lifecycle events Folders with a SKILL.md of instructions Allow, ask and deny rules for tool calls A markdown file of standing instructions
Who enforces it The harness, every time The model, when the description matches The harness The model, as context
Can it block an action Yes, on events that can block No Yes No
Runs code Yes Only through tools Claude already has No No
Best for Formatting, guards, notifications, checks Teaching a repeatable workflow Limiting what Claude may touch Project conventions and commands

The practical split: use CLAUDE.md for conventions, skills for workflows, permissions for the allowed surface, and hooks for anything that must happen or must not happen regardless of the model. If I find myself writing "always" or "never" in a CLAUDE.md rule and it keeps getting ignored, that rule probably wants to be a hook. I wrote about building skills in the skill developer guide.

A Notification hook is a cousin of the status line, which shows state continuously instead of alerting you once. And SubagentStart and SubagentStop let you hook into delegated work, which is covered in the subagents guide.

If a hook does nothing, run /hooks to confirm it is listed, validate the JSON (no trailing commas), check the script is executable, and make sure nothing else prints to stdout before your JSON. Pipe sample JSON into a script to test it outside a session, for example echo '{"tool_input":{"command":"rm -rf /tmp/x"}}' | .claude/hooks/block-dangerous.sh, then check echo $?.

Frequently Asked

What are Claude Code hooks? They are user-defined handlers that Claude Code runs at specific lifecycle events, such as before a tool call, after an edit, or when Claude finishes responding. Most are shell commands, and because the harness runs them, they fire every time instead of depending on the model.

Where do I put Claude Code hooks? In a settings file: ~/.claude/settings.json for all projects, .claude/settings.json for one project (committed), or .claude/settings.local.json for one project (gitignored). Plugins can ship them in hooks/hooks.json, and skills and subagents can define them in frontmatter.

How do I block a tool call with a hook? Use a PreToolUse hook. Exit with code 2 and write the reason to stderr, or exit 0 and print JSON with permissionDecision set to deny inside hookSpecificOutput. Either way, Claude Code stops the call before it runs.

Can a hook run when Claude finishes a task? Yes, with the Stop event. It fires whenever Claude finishes responding, not only at task completion, so check stop_hook_active in the input to avoid loops.

Do hooks work with --dangerously-skip-permissions? A PreToolUse hook that denies a call still blocks it in bypassPermissions mode or with that flag, according to the hooks guide. That makes hooks useful for policy people cannot switch off by changing permission mode.

Sources: Claude Code hooks reference and Automate actions with hooks (hooks guide), checked October 23, 2026.

來源:DEV Community · dev.to