← Blog

Claude Code Hooks: Events, Matchers, and Examples

September 30, 2026

AI Coding

Claude Code hooks are commands you register to run automatically at specific points in a session: before a tool call, after a file edit, when Claude stops, when a session starts. The point is determinism. An instruction in a prompt is something Claude usually follows. A hook is something that always runs.

That makes hooks the right tool for a narrow set of jobs, and the wrong tool for most others:

You want to…UseWhy
Format every file Claude editsHook (PostToolUse)It has to happen every time, not when Claude remembers
Stop Claude touching .env or .git/Hook (PreToolUse) or a permission deny ruleA hook can inspect the path and explain the block
Teach Claude your project's conventionsCLAUDE.mdThat's guidance Claude should weigh, not a gate
Always allow or always deny a toolPermission rulesThe permission system is the hard boundary; hooks can't loosen it
Give Claude a repeatable multi-step taskA skillSkills are instructions Claude invokes; hooks never run tools themselves

Everything below comes from Anthropic's hooks guide and the hooks reference, checked as of September 2026. Claude Code adds hook events and fields regularly, so treat the reference as the source of truth for anything version-specific.

How Claude Code hooks work

A hook has three levels of nesting, and the docs use a precise name for each:

  1. Hook event: the lifecycle point, like PreToolUse or Stop
  2. Matcher group: a filter on when it fires, like "only for the Bash tool"
  3. Hook handler: the thing that runs

When an event fires, Claude Code runs every matching handler in parallel. One handler's deny doesn't stop its siblings from running.

Most handlers are "type": "command", a shell command that gets the event on stdin. The other four types are http (POST the event to a URL), mcp_tool (call a tool on a configured MCP server), prompt (ask a Claude model for a single-turn decision), and agent (spawn a subagent that can inspect files before deciding, still experimental). Start with command hooks; the docs recommend them for production.

The hook events

As of September 2026, the reference lists 33 hook events. You won't need most of them, but check the list before you build a workaround for something that already has one:

WhenEvents
Session lifecycleSetup, SessionStart, SessionEnd
Each turnUserPromptSubmit, UserPromptExpansion, Stop, StopFailure, MessageDisplay
Around tool callsPreToolUse, PermissionRequest, PermissionDenied, PostToolUse, PostToolUseFailure, PostToolBatch
Subagents, tasks, and teamsSubagentStart, SubagentStop, TaskCreated, TaskCompleted, TeammateIdle
Context and modelInstructionsLoaded, PreCompact, PostCompact, PreModelSwitch, PostModelSwitch
Files, config, and workspaceConfigChange, CwdChanged, DirectoryAdded, FileChanged, WorktreeCreate, WorktreeRemove
Notifications and MCPNotification, Elicitation, ElicitationResult

Four carry most real-world setups:

  • PreToolUse fires before a tool runs and can block it. Guardrails live here.
  • PostToolUse fires after a tool succeeds. It can't undo anything, so it's for formatting and linting.
  • SessionStart can add text to Claude's context. With a compact matcher it re-injects what auto-compaction summarised away.
  • Stop fires whenever Claude finishes responding, not only when a task is complete. API errors fire StopFailure instead.

Where hook settings live

Where you define a hook decides who it applies to:

LocationApplies toShareable
~/.claude/settings.jsonAll your projectsNo, local to your machine
.claude/settings.jsonOne projectYes, commit it to the repo
.claude/settings.local.jsonOne projectNo, gitignored when Claude Code saves a setting to it
Managed policy settingsThe whole organisationAdmin-controlled
A plugin's hooks/hooks.jsonWhile the plugin is enabledYes, bundled with the plugin
Skill frontmatterThe rest of the session once the skill is invokedYes, in the skill file
Subagent frontmatterWhile that subagent runsYes, in the subagent file

Two details trip people up. Hook entries merge across levels, so a project hook doesn't cancel your user hook. And hooks from settings files and plugins also fire inside subagents, with an agent_id in the input so your script can tell.

The settings docs cover file precedence. Type /hooks to browse every configured hook and the file it came from; the menu is read-only, so edits happen in the JSON. Setting "disableAllHooks": true turns hooks off without deleting them, though it can't touch hooks an administrator set in managed settings.

And cloud sessions don't read your local ~/.claude/settings.json. A hook that must follow a repo into the cloud belongs in the project's .claude/settings.json.

Matchers: which hooks fire

Without a matcher, a hook fires on every occurrence of its event. What a matcher filters depends on the event: the tool name for tool events, how the session began for SessionStart (startup, resume, clear, compact, fork). How Claude Code reads the string depends on its characters:

MatcherRead asExample
"*", "", or omittedMatch everythingEvery occurrence
Letters, digits, _, -, spaces, commas, and pipes onlyExact name, or a list of exact namesEdit|Write or Edit, Write matches exactly those two tools
Anything elseA JavaScript regular expression, unanchoredmcp__github__.* matches every tool on the github MCP server

Unanchored matters: Edit.* also matches NotebookEdit, so write ^Edit$ when you mean one tool. Some events, including UserPromptSubmit and Stop, don't support matchers at all, and a matcher on them is silently ignored.

For tool events there's a finer filter. The if field on a handler uses permission rule syntax, so "Bash(git *)" runs the hook only when a Bash command includes a git subcommand, and "Edit(*.ts)" runs it only for TypeScript files. The docs call this filter best-effort, which is why hard allow and deny rules still belong in the permission system.

What a hook receives, and how it blocks

A command hook gets the event as JSON on stdin: common fields like session_id and cwd, plus fields specific to the event. Trimmed, here's what a PreToolUse hook sees before Claude runs npm test:

{
  "session_id": "abc123",
  "cwd": "/home/user/my-project",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": { "command": "npm test" }
}

Your script answers with an exit code, and the exit codes don't mean what Unix habit suggests:

Exit codeWhat happens
0No objection. For PreToolUse this does not approve the call; the normal permission flow still runs
2Block. Stderr becomes the reason. On PreToolUse, Claude gets it as feedback and can adjust
Anything else, including 1Without valid JSON output, a non-blocking error for most events. The action goes ahead and the transcript shows a hook error notice

That last row is the trap. A policy hook that exits 1 on failure enforces nothing. If a hook is meant to stop something, it has to exit 2.

For finer control, exit 0 and print JSON to stdout instead. A PreToolUse hook can return a permissionDecision of allow, deny, or ask inside hookSpecificOutput, along with a reason Claude will read. When several hooks answer the same call, the most restrictive answer wins.

Here's a trimmed version of the guide's pattern for protecting sensitive files. Register the hook in .claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
          }
        ]
      }
    ]
  }
}

Then have the script read the path from stdin and refuse the protected ones:

#!/bin/bash
FILE_PATH=$(jq -r '.tool_input.file_path // empty')

for pattern in ".env" "package-lock.json" ".git/"; do
  if [[ "$FILE_PATH" == *"$pattern"* ]]; then
    echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
    exit 2
  fi
done

exit 0

On macOS and Linux, make it executable with chmod +x. The docs' examples parse input with jq.

Claude Code hooks examples worth copying

The guide ships ready-made configurations for the common jobs:

GoalEvent and matcherWhat the command does
Format after every editPostToolUse, Edit|WritePasses the edited path to Prettier
Desktop alert when Claude needs youNotification, empty matcherRuns your OS notification command
Restore key context after compactionSessionStart, compactEchoes reminders that land in Claude's context
Log every shell commandPostToolUse, BashAppends tool_input.command to a log file
Audit settings editsConfigChange, empty matcherWrites source and file path to an audit log

The formatter is the one to start with, because it's harmless and you'll see it work:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "f=$(jq -r '.tool_input.file_path // empty'); [ -n \"$f\" ] || exit 0; npx prettier --write -- \"$f\""
          }
        ]
      }
    ]
  }
}

Reading the path into a quoted variable keeps it as one argument, so a file like my project/a.ts isn't split in two, and -- stops Prettier from reading a path that starts with a dash as an option.

If your settings file already has a hooks key, add the new event inside it rather than replacing the object. And remember that Claude can also change files through shell commands, which an Edit|Write matcher never sees; the docs suggest a Stop hook that scans the working tree when you need full coverage.

The security side of hooks

The reference is blunt: command hooks execute shell commands with your full user permissions, so they can modify, delete, or read anything your account can. Review every hook before you add it, especially ones that arrive inside a repo or a plugin.

Workspace trust is the safety net, and it has a gap you should know about:

Session typeHooks from settings files
InteractiveHeld back until you accept the workspace trust dialog for the folder
claude -p or SDKThe folder is treated as trusted, so a repo's committed hooks run

So before you script claude -p over a repository you didn't write, read its .claude/ settings first, or turn hooks off for that one run by passing --settings with disableAllHooks set to true.

The docs' own best practices are short: validate hook input, quote shell variables ("$VAR", not $VAR), reject paths containing .., use absolute script paths, and stay away from .env, .git/, and keys.

Some behaviour works in your favour. A PreToolUse deny blocks the tool in every permission mode, including bypassPermissions, and a hook's allow can't override a deny rule. Hooks tighten restrictions; they never loosen them. Some works against you: a command hook that times out on PreToolUse doesn't block the call, and a mistyped script path fails as a non-blocking error. A broken gate looks exactly like an open one, so watch the first run of any policy hook.

FAQ

What are Claude Code hooks?

User-defined handlers, usually shell commands, that Claude Code runs automatically at fixed points in a session, such as before a tool call. They guarantee an action happens instead of relying on Claude to choose it.

Where do I put Claude Code hook settings?

In the hooks object of a settings file: ~/.claude/settings.json for every project, .claude/settings.json to share with a repo, .claude/settings.local.json for yourself only.

How does a PreToolUse hook block a command?

Exit with code 2 and write the reason to stderr, or exit 0 and print JSON with a permissionDecision of deny. Exit code 1 doesn't block for most events.

Why isn't my hook firing?

Run /hooks to confirm it's registered and see which file it came from. Then check that the matcher spells the tool name exactly, that the event supports matchers at all, and that the script path exists and is executable. In an interactive session, hooks wait until you've trusted the folder.

What's the difference between hooks and CLAUDE.md?

A CLAUDE.md file is guidance Claude reads and weighs. A hook is code that runs whether Claude agrees or not. Put conventions in CLAUDE.md and put anything that must happen every time in a hook.

The short version

Hooks are Claude Code's escape hatch from "please remember to". Pick the event, narrow it with a matcher, and exit 2 when you mean no. Start with a formatter, add a PreToolUse guard for the files you care about, and read every hook someone else wrote before it runs on your machine.

A good Claude Code setup is mostly configuration someone got right once: hooks, permissions, skills, subagents. Taku is for people who'd rather start from a setup that already works than rebuild it file by file. It's an AI-native desktop workspace where you mirror an AI app, skill, or workflow someone else proved out, run it, and remix it into your own. Taku is in Beta, and the Mac app is available now.