Claude Code hooks are commands that Claude Code runs every time an event fires, such as before a tool call or when a session ends, set under hooks in settings.json. Exit code 2 or a JSON deny blocks the action; any other failure lets it go ahead.
Claude CodeFable 5.1 (max) (with fallback)70.4 · Coding Agent Index
CodexGPT-6 Astra (max)67.0 · Coding Agent Index
Muse CodeMuse Spark 1.3 (xhigh)64.2 · Coding Agent Index
Grok BuildGrok 4.5 (high)64.1 · Coding Agent Index
Kimi Code CLIKimi K362.6 · Coding Agent Index
OpencodeGemini 3.8 Flash (high)61.2 · Coding Agent Index
Cursor CLIComposer 2.5 Fast38.3 · Coding Agent Index
Demo setups use current model families. The pairings above are the combinations benchmarked by Artificial Analysis; rankings measure performance, not popularity.
Illustrative daily trends · Other totals: last 30 days. Build Hours: this week, model-estimated activity including overlap; not engineer hours.
USED BY ENGINEERING TEAMS AT
What are Claude Code hooks?
A hook is a handler that runs every time its event fires: before a tool call, after a file edit, when you submit a prompt, when the session ends. A CLAUDE.md instruction is advice Claude may follow; a hook always runs.
Most hooks are shell commands: Claude Code sends the event as JSON on stdin, and the command answers with an exit code and, optionally, JSON on stdout. Other handler types post the event to a URL (http), call an MCP tool, or ask a model or a subagent.
This .claude/settings.json blocks edits to protected files before they happen and runs Prettier on every file Claude writes. Both commands read the event with jq, so it has to be on your PATH:
#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | 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
Where hooks are configured
Hooks sit under the hooks key of a settings file, never in a file of their own: only plugins use a separate hooks/hooks.json. Hooks from every location run, so a project hook adds to yours instead of replacing it. /hooks lists every active hook and where it came from.
Command hooks run with your full user permissions. Settings-file hooks wait for the folder's trust dialog, but claude -p skips it, so read a repository's .claude/settings.json before you script Claude over code you didn't write.
The hooks reference lists more than 30 events. A matcher picks which occurrences fire a group: the tool name for tool events, the source or reason for session events. Matchers are case-sensitive. One made only of letters, digits, _, -, | and , lists exact names, such as Edit|Write; any other character makes it an unanchored regex, so Edit.* also matches NotebookEdit. MCP tools are named mcp__server__tool, so mcp__github__.* covers one server.
On tool events, a handler's if field filters further with permission-rule syntax, such as Bash(git *) or Edit(*.ts), so the command doesn't even start for other calls.
The events most hooks use, from Anthropic's hooks reference. The last column is what exit code 2 does.
Event
Fires
Matcher
Exit 2
SessionStart
A session starts or resumes
startup, resume, clear, compact, fork
Can't block
UserPromptSubmit
You send a prompt
None
Rejects the prompt
PreToolUse
Before a tool call
Tool name
Blocks the call
PermissionRequest
Before a permission prompt
Tool name
Ignored: deny with JSON
PostToolUse
After a tool call succeeds
Tool name
Can't undo; stderr goes to Claude
Stop
Claude finishes responding
None
Claude keeps working
Notification
Claude Code sends a notification
Notification type
Ignored
SessionEnd
The session ends
clear, resume, logout and more
Can't block
How a hook allows or blocks
A hook answers with its exit code (the table below) or, for finer control, exits 0 and prints JSON. A PreToolUse hook sets permissionDecision to allow, deny, ask or defer; when hooks disagree, deny wins over defer, ask and allow, and updatedInput replaces the tool's whole input. A deny holds even in bypassPermissions mode, but an allow can't override a deny rule in settings. This output denies the call:
stdout of a PreToolUse hook
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Database writes are not allowed"
}
}
Command hook exit codes, from Anthropic's hooks reference.
Exit code
What happens
0
Success. Stdout that starts with { is read as JSON; for UserPromptSubmit and SessionStart, plain text is added to Claude's context
2
Blocks, on the events that can block. Stderr, or the JSON reason, tells Claude why, and a JSON allow can't override it
Any other, including 1
A non-blocking error: the action goes ahead and the transcript shows stderr's first line
When a hook fails quietly
Because only exit code 2 blocks, a broken hook stops nothing. One of our repositories runs its formatter in a PostToolUse hook after every Edit and Write; on a laptop without pnpm it failed 124 times in two days (exit code 127, command not found), and Claude carried on each time, so the formatter never ran.
Run a hook's command in your own shell before you commit it, with a sample event on stdin. In a session, /hooks shows whether it loaded, and claude --debug writes each match, exit code and output to ~/.claude/debug/<session-id>.txt.
Common hook problems, from Anthropic's hooks guide and configuration-debugging docs.
Symptom
Usual cause
Never fires
A lowercase or array matcher, hooks in a file of their own, or hooks in ~/.claude.json instead of ~/.claude/settings.json
"hook error" in the transcript
The script isn't executable (chmod +x), the path is wrong, or jq is missing
JSON decision ignored
Your shell profile prints text before the JSON, or permissionDecision sits outside hookSpecificOutput
A Stop hook loops
Exit 0 when the input's stop_hook_active is true; Claude Code overrides a Stop hook after eight blocks in a row
Does Codex have hooks?
Yes, on by default. Codex reads them from a hooks.json or [hooks] tables in config.toml, in ~/.codex or the repository's .codex folder, and a Codex plugin can ship its own. The events mirror Claude Code's: SessionStart, UserPromptSubmit, PreToolUse, PermissionRequest, PostToolUse, Stop, the subagent and compaction events, and a few more. A hook you add yourself runs only after you review and trust it in /hooks.
Blocking works the same way: exit code 2 with a reason on stderr, or JSON with permissionDecision set to deny. An ask decision is parsed but not supported yet. Zest's Codex plugin uses two hooks, SessionStart and UserPromptSubmit, to keep its login fresh.
Does Cursor have hooks?
Yes. Cursor reads .cursor/hooks.json in the project and ~/.cursor/hooks.json, plus enterprise and team hooks that take precedence. Its events have their own names: beforeShellExecution, beforeMCPExecution, beforeReadFile, afterFileEdit, beforeSubmitPrompt, stop, and generic preToolUse and postToolUse.
Exit code 2 blocks here too, and other failures let the action go ahead unless the hook sets failClosed. Cursor also runs the hooks in Claude Code's settings files, mapping their events to its own, while its setting for third-party configs is on, which it is by default. A hook written for Claude Code can then fire in both tools, so test it in each.
Hook examples from a real plugin: Zest
Zest builds AI coding analytics from three hooks: each session's models, tokens, skills, subagents and MCP tools, next to the branch, commits and pull request it touched, shown as Claude Code usage per engineer.
This is the plugin's hooks/hooks.json. PostToolUse has no matcher, so it runs after every tool call. Each hook has a timeout, exits 0 and returns no decision: Zest records, and never blocks or changes what Claude does.
Starter is free (3 seats, no credit card). Pro is $49 a month with 5 seats and Max is $149 a month with 10 seats — priced per workspace, not per engineer, with extra seats at $19 a month.
A skill is instructions Claude chooses to use when a request matches it. A hook is code Claude Code runs every time its event fires, whatever Claude decides, so use a hook for anything that must always happen.
Why isn't my Claude Code hook running?+
Run /hooks: a hook that isn't listed didn't load, usually because it sits outside a settings file's hooks key or its matcher is an array. If it is listed, check that the matcher spells the tool name exactly (Bash, not bash), then start claude --debug and read the log.
Do hooks run inside subagents?+
Yes. Hooks from settings files, managed settings and plugins fire for a subagent's tool calls too, and the input carries agent_id and agent_type so a script can tell them apart.
Can I turn all hooks off?+
Yes. Set disableAllHooks to true in your settings, or pass --settings '{"disableAllHooks": true}' for one run; hooks from managed settings keep running. Administrators can go the other way with allowManagedHooksOnly, which blocks user, project, local and plugin hooks.