Claude Code Hooks Generator
Pick an event, set a matcher, write the command. The settings.json block updates as you go. Everything runs in this tab.
Hook
Added
settings.json
Merge the "hooks" key into your existing settings file. Do not replace the whole file. Handler type is command here; the docs also list http, mcp_tool, prompt and agent handlers.
10 examples you can load
Each button adds the hook to the generator above. Read the command before you keep it.
| Example | Event and matcher | Needs | |
|---|---|---|---|
| Block rm -rf Exit code 2 from a PreToolUse hook blocks the call and feeds stderr back to Claude. | PreToolUse Bash | jq | |
| Protect .env files Stops Read, Edit and Write on .env and .env.* files. A Read deny rule in permissions does the same without a script; use this when you want a custom message. | PreToolUse Read|Edit|Write | jq | |
| Prettier after Edit and Write Formats the file Claude just changed. PostToolUse cannot block, so a Prettier failure never stops the session. | PostToolUse Edit|Write | jq, npx, Prettier in the project | |
| macOS notification when Claude needs you The Notification event fires on permission prompts and idle prompts. | Notification | macOS (osascript) | |
| macOS notification when Claude finishes Stop fires when Claude finishes responding. Do not exit 2 here unless you want Claude to keep going. | Stop | macOS (osascript) | |
| Log every Bash command Appends each command to a file you can audit later. | PreToolUse Bash | jq | |
| Block git push --force Uses the handler-level if field so the script only runs for matching Bash calls. | PreToolUse Bash | none | |
| Add git status to session context SessionStart hooks can return hookSpecificOutput.additionalContext as JSON on stdout. | SessionStart startup|resume | jq, git | |
| Type-check after TypeScript edits The if field limits the hook to .ts files. Output goes to the transcript; PostToolUse cannot block. | PostToolUse Edit|Write | TypeScript in the project | |
| Back up the transcript before compaction transcript_path is a common input field. Copy it before PreCompact runs so you keep the full history. | PreCompact | jq |
How a hook runs
When an event fires, Claude Code looks up the groups registered for it, filters them by matcher, and runs each handler. A command handler gets the event as JSON on stdin. Its exit code and output tell Claude Code what to do next.
- Exit 0: success. Stdout can carry JSON such as
hookSpecificOutput.additionalContext. - Exit 2: a blocking error on events that can block. Stderr goes back to Claude. On events that cannot block, stderr is only shown.
- Any other exit code: a non-blocking error. Execution continues.
The shape of the JSON
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "./check.sh", "timeout": 30 }
]
}
]
}
}Events map to an array of groups. Each group has an optional matcher and a hooks array of handlers. Optional handler fields listed in the docs include if, timeout, statusMessage, async and once.
Events and what the matcher compares
| Event | Matcher compares | Exit code 2 |
|---|---|---|
SessionStart | How the session started: startup, resume, clear, compact, fork | No effect (stderr is shown, the action is not blocked) |
Setup | CLI flag that triggered setup: init, maintenance | No effect (stderr is shown, the action is not blocked) |
UserPromptSubmit | No matcher | Blocks the prompt |
UserPromptExpansion | Command or skill name: your skill name | Blocks the expansion |
PreToolUse | tool_name: Bash, Edit|Write, mcp__memory__.* | Blocks the tool call |
PermissionRequest | tool_name: Bash, Edit|Write | No effect (stderr is shown, the action is not blocked) |
PermissionDenied | tool_name: Bash | No effect (stderr is shown, the action is not blocked) |
PostToolUse | tool_name: Edit|Write | No effect (stderr is shown, the action is not blocked) |
PostToolUseFailure | tool_name: Bash | No effect (stderr is shown, the action is not blocked) |
PostToolBatch | No matcher | Stops the agentic loop |
Notification | Notification type: permission_prompt, idle_prompt, auth_success, elicitation_dialog | No effect (stderr is shown, the action is not blocked) |
MessageDisplay | No matcher | No effect (stderr is shown, the action is not blocked) |
SubagentStart | Agent type: general-purpose, Explore, Plan, or a custom agent name | No effect (stderr is shown, the action is not blocked) |
SubagentStop | Agent type: general-purpose, Explore, Plan, or a custom agent name | Prevents the subagent from stopping |
TaskCreated | No matcher | Rolls back task creation |
TaskCompleted | No matcher | Prevents completion |
Stop | No matcher | Prevents Claude from stopping |
StopFailure | API error type: rate_limit, overloaded, authentication_failed, billing_error | No effect (stderr is shown, the action is not blocked) |
TeammateIdle | No matcher | Prevents the teammate from going idle |
InstructionsLoaded | Load reason: session_start, nested_traversal, path_glob_match, include, compact | No effect (stderr is shown, the action is not blocked) |
ConfigChange | Config source: user_settings, project_settings, local_settings, policy_settings, skills | Blocks the config change |
CwdChanged | No matcher | No effect (stderr is shown, the action is not blocked) |
DirectoryAdded | How the directory was added: slash_command, register_repo_root | No effect (stderr is shown, the action is not blocked) |
FileChanged | Literal file names to watch: .envrc|.env | No effect (stderr is shown, the action is not blocked) |
WorktreeCreate | No matcher | Any non-zero exit fails creation |
WorktreeRemove | No matcher | Any non-zero exit fails removal if the directory exists |
PreCompact | Compaction trigger: manual, auto | Blocks compaction |
PostCompact | Compaction trigger: manual, auto | No effect (stderr is shown, the action is not blocked) |
PreModelSwitch | Target model name: claude-opus-5, .*opus.* | Blocks the model switch |
PostModelSwitch | Target model name: .*opus.* | No effect (stderr is shown, the action is not blocked) |
Elicitation | MCP server name: your server name | Declines the request |
ElicitationResult | MCP server name: your server name | Blocks the response |
SessionEnd | End reason: clear, resume, logout, prompt_input_exit, other | No effect (stderr is shown, the action is not blocked) |
Checked against code.claude.com/docs/en/hooks on 2026-10-20. The docs change often, so confirm the event you depend on. unverified: whether SessionEnd blocks nothing in every version; treat it as informational.
Pitfalls
- Hooks run with your user permissions and no sandbox. A hook script that reads untrusted input needs the same care as any shell script.
- Quote file paths.
tool_input.file_pathcan contain spaces. - A hook that prints extra text on stdout can break JSON parsing. Redirect noisy tools to stderr or /dev/null.
- After you edit a settings file, start a new session to be sure the change applies. unverified: whether a running session reloads hooks on its own.
FAQ
Where do Claude Code hooks go?
In the hooks key of a settings file: ~/.claude/settings.json for all projects, .claude/settings.json for a shared project, or .claude/settings.local.json for your own uncommitted overrides.
How do I block a tool call with a hook?
Use a PreToolUse hook and exit with code 2. Whatever the script writes to stderr is fed back to Claude as the reason. You can also print JSON with hookSpecificOutput.permissionDecision set to deny.
Which hook events can block?
UserPromptSubmit, PreToolUse, Stop, SubagentStop, PreCompact, and several others can block with exit code 2. PostToolUse, Notification, SessionStart, SessionEnd and StopFailure cannot: the action already happened or the event is informational.
What does the matcher do?
The matcher filters when a group fires. For tool events it is compared with the tool name (Bash, Edit|Write, mcp__server__.*). For other events it is compared with a different field, such as the session start source. Events like UserPromptSubmit and Stop have no matcher.
What does a hook receive?
A JSON object on stdin with fields such as session_id, transcript_path, cwd, hook_event_name, and event-specific fields like tool_name and tool_input. Read it with jq in a shell command.