Hooks are event-driven automation that fires on specific Claude Code events. Unlike CLAUDE.md instructions (which are non-deterministic), hooks execute deterministically — they run 100% of the time when triggered.
How hooks work
Hooks let you run commands, call HTTP endpoints, or invoke AI evaluations automatically in response to Claude Code events. Use them for linting, formatting, security checks, logging, and anything else that should happen consistently without relying on Claude to remember.
Key behaviors:
- All matching hooks for an event run in parallel
- Identical handlers are deduplicated automatically (by command string or URL)
- Hooks run in the current directory with Claude Code’s environment
- Settings file changes are picked up automatically by a file watcher
Configuration format
Hooks are configured in settings JSON files under the hooks key. Each event
maps to an array of matcher groups, and each matcher group contains an array of
hook handlers.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/path/to/my-script.sh",
"timeout": 30
}
]
}
]
}
}
Structure breakdown
hooks
└── EventName (e.g., "PreToolUse")
└── Array of matcher groups
├── matcher: regex to filter when this group activates
└── hooks: array of hook handlers
├── type: "command" | "http" | "prompt" | "agent"
├── if: permission rule filter (tool events only)
├── timeout: seconds before timeout
├── statusMessage: custom spinner text
├── once: run only once per session (boolean)
└── [type-specific fields]
Event reference
Blocking events
These events pause execution until hooks complete. Hooks can block or modify the operation.
| Event | Fires when | Matcher matches against |
|---|---|---|
PreToolUse | Before Claude runs a tool | Tool name (Bash, Edit, mcp__.*) |
PermissionRequest | When Claude needs permission | Tool name |
UserPromptSubmit | When you submit a prompt | No matcher support |
Stop | When Claude finishes responding | No matcher support |
SubagentStop | When a subagent finishes | Agent type (Explore, Bash, Plan) |
TaskCreated | When a task is created | No matcher support |
TaskCompleted | When a task completes | No matcher support |
TeammateIdle | When a teammate goes idle | No matcher support |
ConfigChange | When settings change | Config source (project_settings, etc.) |
Elicitation | When MCP server shows a form | MCP server name |
ElicitationResult | When form is submitted | MCP server name |
WorktreeCreate | When creating a worktree | No matcher support |
Non-blocking events
These events fire-and-forget. Hooks run but do not pause Claude’s execution.
| Event | Fires when | Matcher matches against |
|---|---|---|
PostToolUse | After Claude runs a tool | Tool name |
PostToolUseFailure | After a tool fails | Tool name |
PermissionDenied | When auto mode denies a tool call | Tool name |
Notification | When Claude sends a notification | Notification type (permission_prompt, idle_prompt) |
SubagentStart | When a subagent starts | Agent type |
SessionStart | When a session begins | Session source (startup, resume, clear) |
SessionEnd | When a session ends | End reason (clear, resume, logout) |
StopFailure | When Claude stops due to error | Error type (rate_limit, billing_error, etc.) |
CwdChanged | When working directory changes | No matcher support |
FileChanged | When a watched file changes | Filename (basename) |
PreCompact | Before context compaction | Trigger (manual, auto) |
PostCompact | After context compaction | Trigger (manual, auto) |
InstructionsLoaded | When a CLAUDE.md is loaded | Load reason (session_start, include) |
WorktreeRemove | When a worktree is removed | No matcher support |
Hook types
Command hooks
Execute shell commands. The most common type. Receive event data as JSON on stdin and communicate results via exit codes and stdout.
{
"type": "command",
"command": "./scripts/lint-check.sh",
"async": false,
"timeout": 600
}
command(required): Shell command to runasync(optional): Run in background without blocking (default:false)shell(optional):"bash"or"powershell"(default:"bash")- Default timeout: 600 seconds
HTTP hooks
Send a POST request with event data as JSON body.
{
"type": "http",
"url": "http://localhost:8080/hooks/pre-tool-use",
"headers": {
"Authorization": "Bearer $MY_TOKEN"
},
"allowedEnvVars": ["MY_TOKEN"],
"timeout": 30
}
url(required): Endpoint URLheaders(optional): HTTP headers (supports$VARinterpolation)allowedEnvVars(optional): Env vars that can be interpolated into headers- Default timeout: 30 seconds
Prompt hooks
Send a single-turn prompt to a Claude model for yes/no decisions.
{
"type": "prompt",
"prompt": "Does this command look safe? $ARGUMENTS",
"model": "haiku",
"timeout": 30
}
prompt(required): Evaluation text ($ARGUMENTSis replaced with event data)model(optional): Model alias to use- Default timeout: 30 seconds
Agent hooks
Spawn a subagent with tool access to verify conditions.
{
"type": "agent",
"prompt": "Review this file change for security issues. $ARGUMENTS",
"model": "sonnet",
"timeout": 60
}
prompt(required): Task description for the agentmodel(optional): Model alias to use- Default timeout: 60 seconds
Matchers and the if field
Matchers
The matcher field is a regex that determines when a matcher group activates. Use "*", "", or omit entirely to match all events of that type.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [{ "type": "command", "command": "./lint.sh" }]
}
]
}
}
For MCP tools, the naming pattern is mcp__<server>__<tool>:
mcp__memory__.*matches all memory server toolsmcp__.*__write.*matches write tools from any server
The if field
The if field provides additional filtering using permission rule syntax. It only works on tool events (PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest).
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(rm *)",
"command": "./scripts/block-rm.sh"
}
]
}
This hook only fires for Bash tool calls whose command matches rm *. The if field prevents the hook process from spawning at all when the pattern does not match.
Command hook input and output
Input (stdin)
Command hooks receive JSON on stdin with these common fields:
{
"session_id": "abc123",
"cwd": "/path/to/project",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test",
"description": "Run test suite"
}
}
The exact fields vary by event. Tool events include tool_name and tool_input. PostToolUse adds tool_response. Stop includes last_assistant_message.
Output (exit codes)
| Exit code | Behavior |
|---|---|
| 0 | Success. JSON output parsed if present |
| 2 | Blocking error. Blocks the operation (tool call denied, prompt rejected, etc.) |
| Other | Non-blocking error. stderr shown in verbose mode; execution continues |
JSON output (exit 0)
{
"continue": true,
"stopReason": "optional message when continue is false",
"suppressOutput": false,
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow|deny|ask",
"permissionDecisionReason": "Reason shown to user"
}
}
Set "continue": false to stop Claude immediately. The hookSpecificOutput fields vary by event — for example, PreToolUse supports permissionDecision and updatedInput, while PostToolUse supports additionalContext.
Practical examples
Auto-lint after file edits
Run your linter every time Claude writes or edits a file:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "npx eslint --fix $(cat | jq -r '.tool_input.file_path')",
"statusMessage": "Running linter..."
}
]
}
]
}
}
Block destructive commands
Prevent Claude from running rm -rf without your approval:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(rm *)",
"command": ".claude/hooks/block-rm.sh"
}
]
}
]
}
}
The hook script (.claude/hooks/block-rm.sh):
#!/bin/bash
COMMAND=$(jq -r '.tool_input.command')
if echo "$COMMAND" | grep -q 'rm -rf'; then
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "rm -rf blocked by hook"
}
}'
else
exit 0
fi
Add context to every prompt
Inject environment information into Claude’s context on every prompt:
{
"hooks": {
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "echo '{\"hookSpecificOutput\":{\"hookEventName\":\"UserPromptSubmit\",\"additionalContext\":\"Current branch: '$(git branch --show-current)'\"}}'",
"statusMessage": "Loading context..."
}
]
}
]
}
}
Log all tool usage via HTTP
Send tool usage to a monitoring endpoint:
{
"hooks": {
"PostToolUse": [
{
"matcher": "",
"hooks": [
{
"type": "http",
"url": "http://localhost:8080/hooks/tool-usage",
"headers": {
"Authorization": "Bearer $HOOK_TOKEN"
},
"allowedEnvVars": ["HOOK_TOKEN"]
}
]
}
]
}
}
Prevent Claude from stopping prematurely
Force Claude to keep going when it tries to stop without running tests:
{
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "prompt",
"prompt": "Review Claude's last message. Did it run the test suite before stopping? If tests were not run and code was changed, respond NO.",
"statusMessage": "Checking if tests were run..."
}
]
}
]
}
}
Environment variables
These environment variables are available to all command hooks:
| Variable | Description |
|---|---|
CLAUDE_PROJECT_DIR | Project root directory |
CLAUDE_CODE_REMOTE | "true" in remote/web environments; unset locally |
SessionStart, CwdChanged, and FileChanged hooks also receive:
| Variable | Description |
|---|---|
CLAUDE_ENV_FILE | Path to a file where you can persist env vars using export statements |
Settings file locations
| File | Scope | Shared via git? |
|---|---|---|
~/.claude/settings.json | All projects (personal) | No |
.claude/settings.json | This project (team) | Yes |
.claude/settings.local.json | This project (personal) | No (gitignored) |
| Managed policy settings | Organization-wide | Admin-controlled |
Team hooks in .claude/settings.json apply to everyone who clones the repo.
Personal hooks in .claude/settings.local.json add to (but cannot remove) team hooks.
To disable all hooks, set "disableAllHooks": true in your settings file. Note that managed policy hooks cannot be disabled by user or project settings.
Browsing configured hooks
Use the /hooks slash command inside Claude Code to browse all configured hooks. It shows every event with a count of configured hooks, lets you drill into matchers and view handler details, and labels each hook with its source: [User], [Project], [Local], [Plugin], [Session], or [Built-in]. The /hooks view is read-only.
Hooks vs. CLAUDE.md instructions
| Aspect | Hooks | CLAUDE.md |
|---|---|---|
| Execution | Deterministic (always runs) | Non-deterministic (Claude may skip) |
| Scope | Shell commands, HTTP calls, AI evaluation | Natural language instructions |
| Best for | Formatting, linting, security gates, logging | Coding conventions, style preferences |
| Failure mode | Blocks the operation | Claude might not follow it |
Rule of thumb: If something must always happen, use a hook. If it is a preference or guideline, use CLAUDE.md.
Quick reference
| What you want | Event | Matcher | Hook type |
|---|---|---|---|
| Lint after file changes | PostToolUse | Edit|Write | command |
| Block dangerous commands | PreToolUse | Bash + if field | command |
| Add context to prompts | UserPromptSubmit | (none) | command |
| Custom notifications | Notification | notification type | command or http |
| Validate before stopping | Stop | (none) | prompt or command |
| Log tool usage | PostToolUse | "" (all tools) | http |
| Monitor MCP tools | PreToolUse | mcp__server__.* | command |
| Set up session environment | SessionStart | (none) | command |
| React to config changes | ConfigChange | config source | command |
Tips
- Start with a
PostToolUselint hook onEdit|Write— it has the highest payoff-to-effort ratio. - Keep hook commands fast. Slow hooks disrupt the interactive workflow.
- Use
"async": trueon command hooks for non-critical operations like logging. - Test hooks manually before adding them to settings — a broken hook on a blocking event will block all operations of that type.
- Use the
iffield to narrow tool-event hooks without spawning a process for every invocation. - Use the
statusMessagefield to show a custom spinner message while your hook runs. - Set
"once": truefor hooks that only need to run once per session (e.g., environment setup). - Combine hooks with permissions: hooks enforce standards deterministically, permissions control what Claude can do interactively.
- Use
/hooksto verify your hooks are loaded and sourced correctly.