Claude Code hooks are commands (or HTTP calls, prompts and a few other handler types) that Claude Code runs automatically at fixed points in a session, such as before a tool runs, when a permission is needed or when Claude finishes responding. You define them under a hooks key in a settings file such as ~/.claude/settings.json. Each hook receives JSON about the event on stdin and can reply through its exit code or a JSON object on stdout, which lets it log, notify, block an action or answer on your behalf.
01How a hook is put together
A hook configuration has three levels:
- The hook event, such as
PreToolUseorStop. - A matcher group, which filters when it fires, such as "only for the Bash tool".
- One or more hook handlers, the thing that actually runs.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "/path/to/lint-check.sh"
}
]
}
]
}
}
This runs a lint script after Claude writes or edits a file. To add a second event, add a sibling key inside the same hooks object.
02Where hooks live
~/.claude/settings.jsonAll your projectsNo, your machine only.claude/settings.jsonOne projectYes, can be committed.claude/settings.local.jsonOne projectNohooks/hooks.jsonWhile the plugin is enabledYesHooks from different files add up rather than replacing each other. Type /hooks in Claude Code to see every active hook and where it came from.
In an interactive session, Claude Code holds back hooks, even your own, until you accept the workspace trust dialog for the folder.
03Handler types
Most hooks are "type": "command", which runs a shell command. There are four others:
http: POSTs the event JSON to a URL and reads the response body.mcp_tool: calls a tool on an MCP server you have configured.prompt: asks a Claude model a single-turn question and reads a JSON decision.agent: starts a subagent that can read files before deciding (experimental).
Command hooks run with your full user permissions, through sh -c on macOS and Linux and Git Bash (or PowerShell) on Windows. Add "async": true to run one in the background; it can then no longer make decisions.
04Matchers
The matcher decides which occurrences of an event run the hook. What it matches against depends on the event: the tool name for tool events, the notification type for Notification, how the session started for SessionStart, and so on.
"*",""or no matcher at all matches everything.- Letters, digits,
_,-, spaces,|and,are treated as exact names.Edit|Writematches either tool. - Anything else is a JavaScript regular expression, so
mcp__memory__.*matches every tool from one MCP server.
Matchers are case-sensitive. Some events, including Stop and UserPromptSubmit, ignore matchers. For tool events, an if field with a permission rule such as "Bash(git *)" narrows a single handler further.
05Input: what your hook receives
Every hook gets JSON on stdin with a few common fields, including session_id, transcript_path, cwd and hook_event_name, plus fields specific to the event. A PreToolUse hook for a Bash call, for example, also gets tool_name and tool_input.command. Read it with jq or any JSON parser:
#!/bin/bash
input=$(cat)
cmd=$(jq -r '.tool_input.command // empty' <<<"$input")
echo "Claude wants to run: $cmd" >> ~/claude-commands.log
exit 0
06Output: exit codes and JSON
A hook talks back in one of two ways.
Exit codes. 0 is success. 2 is a blocking error: on events that can block, the action stops and your stderr becomes the reason. Any other code, including the usual 1, is a non-blocking error, so the action goes ahead and you see a hook error notice. If a hook is meant to enforce a rule, exit 2, not 1.
JSON on stdout. Exit 0 and print a single JSON object for finer control. Some fields work everywhere, such as continue: false to stop Claude entirely. Others are per event: Stop uses decision: "block" with a reason, while PreToolUse and PermissionRequest put their decision inside hookSpecificOutput. Print nothing else on stdout: a stray line from your shell profile stops Claude Code reading the JSON.
07Every hook event
SessionStartA session begins or resumesNo (adds context)SetupStarted with --init-only, or --init / --maintenance with -pNoInstructionsLoadedA CLAUDE.md or .claude/rules/*.md file is loadedNoUserPromptSubmitYou submit a prompt, before Claude sees itYesUserPromptExpansionA typed command expands into a promptYesPreToolUseBefore any tool callYesPermissionRequestA tool call needs a permission decisionAllows or denies through JSONPermissionDeniedAuto mode denies a tool callNoPostToolUseAfter a tool call succeedsNo (tool already ran)PostToolUseFailureAfter a tool call failsNoPostToolBatchAfter a batch of parallel tool calls, before the next model callYesNotificationClaude Code sends a notificationNoMessageDisplayWhile Claude's reply text is displayedNo (can change what is shown)SubagentStart / SubagentStopA subagent starts or finishesSubagentStop onlyTaskCreated / TaskCompletedA task is created or marked completeYesStopClaude finishes respondingYes (keeps Claude going)StopFailureThe turn ends with an API errorNoTeammateIdleAn agent team teammate is about to go idleYesConfigChangeA settings file changes during a sessionYesCwdChangedThe working directory changesNoDirectoryAddedA directory is added with /add-dirNoFileChangedA watched file changes on diskNoWorktreeCreate / WorktreeRemoveA worktree is created or removedYesPreCompact / PostCompactBefore and after context compactionPreCompact onlyPreModelSwitch / PostModelSwitchBefore and after the model changesPreModelSwitch onlyElicitation / ElicitationResultAn MCP server asks for input, and after you answerYesSessionEndA session endsNoAnthropic adds events from time to time, so check the hooks reference for the current list and each event's input fields.
08Which events matter for notifications
Four events cover nearly everything people want to be told about:
Stopfor "Claude has finished". See the Stop hook guide.Notificationfor "Claude is waiting for you", with types such aspermission_promptandidle_prompt. See the Notification hook guide.PermissionRequestto be told, or to answer, the instant a permission is needed. See the PermissionRequest guide.PreToolUseon theAskUserQuestiontool, to see or answer Claude's multiple-choice questions. See answering questions from your phone.
For a ready-to-paste desktop setup, start with how to get a notification when Claude Code finishes.
09Where Claude Code Notifier fits
Claude Code Notifier is built entirely on these hooks. Its free command line tool, ccn, runs on Mac, Windows and Linux. Installing it adds seven hook entries to ~/.claude/settings.json, each running ccn hook, and keeps a one-time backup of the file. ccn runs for a few milliseconds per event, then exits, so nothing stays running.
On its own it shows desktop notifications. With the iPhone or Android app paired, the same hooks carry your answers back: allow or deny a permission, answer a question, or send the next instruction after Claude stops. It is independent and not made by Anthropic. If you only need a log or a lint step, a few lines of your own are simpler. Details are on the ccn page.
10FAQ
How do I test a hook without waiting for Claude?
Pipe sample JSON into the script and check the exit code: echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh; echo $?. Then run /hooks in Claude Code to confirm it is registered under the right event.
Why is my hook not running?
The usual causes are invalid JSON in settings.json (trailing commas, comments), a matcher that doesn't match exactly (they are case-sensitive), a script that isn't executable, or a folder you haven't trusted yet. Start Claude Code with claude --debug and read the debug log to see what ran. The full list is in the troubleshooting checklist.
How do I turn all hooks off for a while?
Set "disableAllHooks": true in a settings file. To skip them for one run, start Claude Code with --settings '{"disableAllHooks": true}'. There is no switch for one hook on its own; you remove its entry instead. Neither can turn off hooks that your organisation set in managed settings.
What is the default hook timeout?
Ten minutes (600 seconds) for command, HTTP and MCP tool hooks, 30 seconds for prompt hooks and 60 for agent hooks. A few events use shorter defaults, and you can set timeout in seconds on any handler.