Claude Code Hooks: Events, Matchers, and Examples
September 30, 2026

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… | Use | Why |
|---|---|---|
| Format every file Claude edits | Hook (PostToolUse) | It has to happen every time, not when Claude remembers |
Stop Claude touching .env or .git/ | Hook (PreToolUse) or a permission deny rule | A hook can inspect the path and explain the block |
| Teach Claude your project's conventions | CLAUDE.md | That's guidance Claude should weigh, not a gate |
| Always allow or always deny a tool | Permission rules | The permission system is the hard boundary; hooks can't loosen it |
| Give Claude a repeatable multi-step task | A skill | Skills 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:
- Hook event: the lifecycle point, like
PreToolUseorStop - Matcher group: a filter on when it fires, like "only for the Bash tool"
- 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:
| When | Events |
|---|---|
| Session lifecycle | Setup, SessionStart, SessionEnd |
| Each turn | UserPromptSubmit, UserPromptExpansion, Stop, StopFailure, MessageDisplay |
| Around tool calls | PreToolUse, PermissionRequest, PermissionDenied, PostToolUse, PostToolUseFailure, PostToolBatch |
| Subagents, tasks, and teams | SubagentStart, SubagentStop, TaskCreated, TaskCompleted, TeammateIdle |
| Context and model | InstructionsLoaded, PreCompact, PostCompact, PreModelSwitch, PostModelSwitch |
| Files, config, and workspace | ConfigChange, CwdChanged, DirectoryAdded, FileChanged, WorktreeCreate, WorktreeRemove |
| Notifications and MCP | Notification, Elicitation, ElicitationResult |
Four carry most real-world setups:
PreToolUsefires before a tool runs and can block it. Guardrails live here.PostToolUsefires after a tool succeeds. It can't undo anything, so it's for formatting and linting.SessionStartcan add text to Claude's context. With acompactmatcher it re-injects what auto-compaction summarised away.Stopfires whenever Claude finishes responding, not only when a task is complete. API errors fireStopFailureinstead.
Where hook settings live
Where you define a hook decides who it applies to:
| Location | Applies to | Shareable |
|---|---|---|
~/.claude/settings.json | All your projects | No, local to your machine |
.claude/settings.json | One project | Yes, commit it to the repo |
.claude/settings.local.json | One project | No, gitignored when Claude Code saves a setting to it |
| Managed policy settings | The whole organisation | Admin-controlled |
A plugin's hooks/hooks.json | While the plugin is enabled | Yes, bundled with the plugin |
| Skill frontmatter | The rest of the session once the skill is invoked | Yes, in the skill file |
| Subagent frontmatter | While that subagent runs | Yes, 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:
| Matcher | Read as | Example |
|---|---|---|
"*", "", or omitted | Match everything | Every occurrence |
Letters, digits, _, -, spaces, commas, and pipes only | Exact name, or a list of exact names | Edit|Write or Edit, Write matches exactly those two tools |
| Anything else | A JavaScript regular expression, unanchored | mcp__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 code | What happens |
|---|---|
| 0 | No objection. For PreToolUse this does not approve the call; the normal permission flow still runs |
| 2 | Block. Stderr becomes the reason. On PreToolUse, Claude gets it as feedback and can adjust |
| Anything else, including 1 | Without 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:
| Goal | Event and matcher | What the command does |
|---|---|---|
| Format after every edit | PostToolUse, Edit|Write | Passes the edited path to Prettier |
| Desktop alert when Claude needs you | Notification, empty matcher | Runs your OS notification command |
| Restore key context after compaction | SessionStart, compact | Echoes reminders that land in Claude's context |
| Log every shell command | PostToolUse, Bash | Appends tool_input.command to a log file |
| Audit settings edits | ConfigChange, empty matcher | Writes 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 type | Hooks from settings files |
|---|---|
| Interactive | Held back until you accept the workspace trust dialog for the folder |
claude -p or SDK | The 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.