A hook is a command you configure that Claude Code itself runs at a fixed point in a session, such as right before a tool call or when Claude finishes answering. The model does not decide whether it runs, so a hook gives you the deterministic control that an instruction in CLAUDE.md cannot.
What it is
Hooks are part of Claude Code’s settings. Each hook ties an event (a moment in Claude Code’s lifecycle) to a handler (what to run). There are more than thirty events. The ones you will use most:
| Event | Fires | Can block |
|---|---|---|
PreToolUse | Before a tool call runs | Yes, the tool call |
PostToolUse | After a tool call succeeds | No, but it can feed errors back to Claude |
PostToolUseFailure | After a tool call fails | No |
PermissionRequest | When Claude Code is about to ask you for permission | Through a JSON decision |
Notification | When Claude is waiting for your input or permission | No |
Stop | When Claude finishes a response | Yes, Claude keeps working |
SessionStart | When a session starts, resumes, or restarts after /clear or compaction | No |
UserPromptSubmit | When you send a prompt, before Claude sees it | Yes, the prompt |
A handler has one of five types:
command: a shell command. The common case.http: POSTs the event data to a URL.mcp_tool: calls a tool on a connected MCP server.prompt: asks a Claude model a yes/no question about the event, for decisions a fixed rule cannot express.agent: starts a subagent that can read files and run commands before deciding. Experimental.
What problem it solves
Instructions in CLAUDE.md are advice. The model usually follows them, but nothing enforces it, and a long session makes slips more likely. Some things should happen every time without anyone remembering: format the file after each edit, run the type checker, refuse to touch .env, ping you when the agent is stuck on a permission prompt. A hook does that the same way on every turn, whatever the model is thinking.
How to try it
Hooks live under a hooks key in a settings file, in three levels: the event, a matcher group that picks which tool (or notification type) it applies to, and the list of handlers. This one runs Prettier on every file Claude edits:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write" }
]
}
]
}
}Run /hooks in a session to check it appears under PostToolUse. The menu is a read-only browser of every configured hook, grouped by event. Edits to the settings file are normally picked up while the session runs.
Matchers
"*",""or no matcher: matches everything.- Letters, digits,
_,-, spaces,,and|only: exact names, or a list such asEdit|Write. - Any other character: a JavaScript regular expression, not anchored, so
Edit.*also matchesNotebookEdit. - MCP tools are named
mcp__<server>__<tool>; match a whole server withmcp__github__.*. - An
iffield narrows a tool hook further with permission-rule syntax, for example"if": "Bash(git *)". It is best-effort, not a security boundary.
Input and output
Claude Code passes the event as JSON on standard input. For a tool event that includes tool_name, tool_input (for Bash, the command; for Edit, the file_path), cwd and session_id.
The hook answers with its exit code:
0: success. Claude Code reads JSON from stdout if there is any.2: block the action, on events that can block. Stderr goes to Claude as the reason, so it can try another way.- Anything else: a hook error. The action goes ahead, and you see the first line of stderr.
Or, on exit 0, the hook prints one JSON object. For PreToolUse the useful fields sit under hookSpecificOutput:
permissionDecision:allow,deny,askordefer.updatedInput: replaces the tool’s arguments before it runs, for example to rewrite a shell command.additionalContext: text placed next to the tool result for Claude to read.
Exit 0 with no output means “no opinion”: the normal permission flow decides.
Where hooks live
| Location | Applies to | Shared |
|---|---|---|
~/.claude/settings.json | All your projects | No |
.claude/settings.json | One project | Yes, committed to the repo |
.claude/settings.local.json | One project | No |
| Managed policy settings | The organization | Set by an admin |
A plugin’s hooks/hooks.json | While the plugin is enabled | With the plugin |
| Skill or subagent frontmatter | While the skill or subagent is active | With the file |
Hooks from all these places add up rather than replace each other, and every hook that matches an event runs in parallel. "disableAllHooks": true turns them all off; there is no switch for one hook that keeps it in the file.
Usage example
Frontend
A Vite + React project wants consistent formatting and no edits to the lockfile. In .claude/settings.json, the Prettier hook above runs after every Edit|Write, and a PreToolUse hook on the same matcher blocks protected paths. The script, in Node so the team needs no jq:
// .claude/hooks/protect-files.ts
let raw = "";
for await (const chunk of process.stdin) raw += chunk;
const input = JSON.parse(raw);
const file: string = input.tool_input?.file_path ?? "";
const protectedParts = [".env", "package-lock.json", ".git/"];
const hit = protectedParts.find((p) => file.includes(p));
if (hit) {
console.error(`Blocked: ${file} matches protected pattern ${hit}`);
process.exit(2); // exit 2 blocks the edit; stderr tells Claude why
}Register it with "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.ts" (Node 24 runs TypeScript directly).
Backend
A Node API repo where agents must never commit to main. A PreToolUse hook on Bash reads tool_input.command, checks the current branch, and exits 2 for git commit or git push on main. Because a crashing hook lets the command through, the hook is started from a small shell launcher that itself exits 2 when the runtime it needs is missing. This is how this wiki’s own repo guards main.
Who it’s for and when
Anyone who keeps telling the agent the same “always” or “never”. Good fits:
- formatting and type checks after edits, see Automating checks with hooks;
- blocking paths or commands, as a second line next to permission rules;
- notifications when a session needs you;
- trimming noisy command output before it reaches the context (see Reducing token usage).
Skip a hook when the rule needs judgment on every case and a wrong block would get in the way; write it in CLAUDE.md instead, or use a prompt hook.
Gotchas
exit 1does not block. Only exit 2 (or a JSONdeny) blocks. A hook that crashes, times out or prints broken JSON lets the action through, unless you set"onFailure": "block"on it (v2.1.295 or later).- Not a hard gate. The
iffilter is best-effort, and a timed-outPreToolUsehook does not block. Use permission rules for a hard allow or deny. - But a
denysticks. A hook’sdenyblocks the tool even inbypassPermissionsmode, and a hook’sallowdoes not override deny rules from settings. @files skipPreToolUse. Files you reference with@are inserted while the prompt is built, without a tool call, so a hook onReadnever sees them. Block such paths with aReaddeny rule.- Two input rewrites race. If two
PreToolUsehooks returnupdatedInputfor the same tool, the one that finishes last wins, and the order is not fixed. PostToolUsecannot undo. The tool already ran; exit 2 only shows stderr to Claude.Stophooks can loop. Claude Code overrides aStophook that blocks eight times in a row with no tool call in between. Checkstop_hook_activein the input and exit early.- Default timeout is 10 minutes for
command,httpandmcp_toolhandlers (30 seconds forprompt, 60 foragent). Settimeoutper hook. - A cloned repo’s hooks run as you. Hooks in a project’s
.claude/settings.jsonrun with your permissions, and inclaude -por the Agent SDK they run even though no trust dialog was ever shown. Read a repo’s hooks before you run an agent in it. - Other tools edit your settings too. iTerm2 notes that Claude Code sometimes rewrites
~/.claude/settings.jsonand can drop the hook its integration added; check/hooksif one goes quiet.
Links
- Automate actions with hooks
- Hooks reference
- Automating checks with hooks: ready-made patterns
- CLAUDE.md: the advice that hooks enforce
- MCP in Claude Code: the tool names hook matchers use