17 event-driven systems for building automation workflows
Hooks are deterministic code that automatically runs when specific events occur in Claude Code. Instead of relying on LLM judgment, they execute reliably according to predefined rules.
Hooks are "automatic reaction rules". They work similarly to smartphone automation apps (e.g., IFTTT).
For example:
Hooks are about setting up rules that say "When something happens → automatically do this".
Claude Code provides a total of 17 Hook events.
| Event | When It Fires | Primary Use |
|---|---|---|
| SessionStart | When a session starts | Environment initialization, welcome message |
| UserPromptSubmit | After user prompt submission | Input validation, transformation |
| PreToolUse | Just before tool execution | Execution blocking, input modification |
| PostToolUse | Right after tool execution | Result processing, auto-formatting |
| PostToolUseFailure | After tool execution failure | Error handling, fallback execution |
| PermissionRequest | When permission is requested | Auto-approve/deny |
| Notification | When a notification occurs | Desktop notifications, logging |
| SubagentStart | When a sub-agent starts | Agent initialization |
| SubagentStop | When a sub-agent stops | Result collection, cleanup |
| Stop | When Claude's response is complete | Result validation, post-processing |
| TeammateIdle | When a teammate agent is idle | Task assignment |
| TaskCompleted | When a task is completed | Completion notification, post-processing |
| ConfigChange | When configuration changes | Config validation, synchronization |
| WorktreeCreate | When a worktree is created | Environment setup, dependency installation |
| WorktreeRemove | When a worktree is removed | Cleanup, temporary file deletion |
| PreCompact | Just before context compaction | Preserving important information |
| SessionEnd | When a session ends | Cleanup, logging, reporting |
To start, knowing just these 3 is enough:
You can learn the rest one at a time as the need arises.
Each Hook event can have 3 types of handlers attached to it.
A handler determines "what actually happens when an event fires."
prettier) → Most commonly usedStarting with just the Command handler is sufficient.
The most common handler that executes shell commands. It receives JSON via stdin and outputs JSON via stdout.
{
"hooks": {
"PostToolUse": [
{
"type": "command",
"command": "npx prettier --write $CLAUDE_FILE_PATH",
"matcher": "Write|Edit"
}
]
}
}Performs a single LLM call. Suitable for yes/no decisions or simple analysis.
{
"hooks": {
"Stop": [
{
"type": "prompt",
"prompt": "Review whether the task just completed fully satisfies the original request. Respond 'no' if anything is missing, or 'yes' if it is complete."
}
]
}
}When the Prompt handler returns "no", Claude performs additional work.
Runs a sub-agent to perform complex validation. A maximum of 50 turns and a 60-second timeout apply.
{
"hooks": {
"Stop": [
{
"type": "agent",
"prompt": "Run tests for all changed files, and fix any failing tests.",
"tools": ["Bash(npm test)", "Read", "Edit"]
}
]
}
}Command handlers receive JSON data via stdin. The fields included vary depending on the event.
{
"session_id": "abc123",
"cwd": "/home/user/project",
"hook_event_name": "PreToolUse"
}{
"session_id": "abc123",
"cwd": "/home/user/project",
"hook_event_name": "PreToolUse",
"tool_name": "Write",
"tool_input": {
"file_path": "/home/user/project/src/index.ts",
"content": "..."
}
}{
"session_id": "abc123",
"cwd": "/home/user/project",
"hook_event_name": "UserPromptSubmit",
"prompt": "The prompt content entered by the user"
}Claude Code's behavior changes depending on the Command handler's exit code.
| Exit Code | Behavior |
|---|---|
| 0 | Proceed normally (if there is stdout output, it is passed to Claude) |
| 2 | Block the action (stop tool execution, deliver error message) |
| Other | Log a warning and proceed |
#!/bin/bash
# protect-files.sh - Block modification of protected files
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Block .env file modifications
if [[ "$FILE_PATH" == *.env* ]]; then
echo '{"error": "Cannot modify .env files."}'
exit 2
fi
exit 0Use matchers to apply Hooks only to specific tools or patterns.
| Type | Description | Example |
|---|---|---|
| Exact string | Matches the tool name exactly | "Write" |
| Regex | Matches using a regex pattern | "Write|Edit" |
| MCP tool | Matches MCP server tools | "mcp__*" |
{
"hooks": {
"PreToolUse": [
{
"type": "command",
"command": "/path/to/check-script.sh",
"matcher": "Write|Edit"
}
],
"PostToolUse": [
{
"type": "command",
"command": "npx prettier --write $CLAUDE_FILE_PATH",
"matcher": "Write"
},
{
"type": "command",
"command": "/path/to/notify-mcp.sh",
"matcher": "mcp__*"
}
]
}
}If no matcher is specified, the Hook runs for all tool calls of that event.
Hooks can be defined in 3 configuration files.
| File | Scope | Git Tracked |
|---|---|---|
~/.claude/settings.json | User global (all projects) | No |
.claude/settings.json | Project (shared with team) | Yes |
.claude/settings.local.json | Project local (personal) | No |
{
"hooks": {
"SessionStart": [
{
"type": "command",
"command": "echo 'Session has started.'"
}
],
"PreToolUse": [
{
"type": "command",
"command": "/path/to/guard.sh",
"matcher": "Write|Edit"
}
],
"PostToolUse": [
{
"type": "command",
"command": "npx prettier --write $CLAUDE_FILE_PATH",
"matcher": "Write"
}
],
"Stop": [
{
"type": "prompt",
"prompt": "Verify the task results."
}
]
}
}Display desktop notifications when Claude completes a task or sends a notification.
{
"hooks": {
"Notification": [
{
"type": "command",
"command": "notify-send 'Claude Code' \"$CLAUDE_NOTIFICATION\" --icon=terminal"
}
]
}
}On macOS, use osascript.
{
"hooks": {
"Notification": [
{
"type": "command",
"command": "osascript -e 'display notification \"Task completed\" with title \"Claude Code\"'"
}
]
}
}Automatically run Prettier after writing or editing a file.
{
"hooks": {
"PostToolUse": [
{
"type": "command",
"command": "npx prettier --write $CLAUDE_FILE_PATH 2>/dev/null || true",
"matcher": "Write|Edit"
}
]
}
}You can also apply ESLint auto-fix alongside it.
{
"hooks": {
"PostToolUse": [
{
"type": "command",
"command": "npx prettier --write $CLAUDE_FILE_PATH && npx eslint --fix $CLAUDE_FILE_PATH 2>/dev/null || true",
"matcher": "Write|Edit"
}
]
}
}Block modifications to specific files or directories.
#!/bin/bash
# guard-protected-files.sh
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Protected patterns
PROTECTED_PATTERNS=(
"*.env"
"*.env.*"
"package-lock.json"
"yarn.lock"
"dist/*"
"node_modules/*"
)
for PATTERN in "${PROTECTED_PATTERNS[@]}"; do
if [[ "$FILE_PATH" == $PATTERN ]]; then
echo "{\"error\": \"Protected file: $FILE_PATH\"}"
exit 2
fi
done
exit 0Connect this script in your settings.
{
"hooks": {
"PreToolUse": [
{
"type": "command",
"command": "/path/to/guard-protected-files.sh",
"matcher": "Write|Edit"
}
]
}
}Re-inject important information to prevent loss during context compaction.
{
"hooks": {
"PreCompact": [
{
"type": "command",
"command": "echo '{\"inject\": \"Rules to always remember: 1) Use TypeScript strict mode 2) Write JSDoc comments for all functions 3) Maintain test coverage above 80%\"}'"
}
]
}
}A script that automatically re-injects key content from CLAUDE.md is also useful.
#!/bin/bash
# reinject-context.sh
CLAUDE_MD="$CLAUDE_CWD/CLAUDE.md"
if [[ -f "$CLAUDE_MD" ]]; then
CONTENT=$(cat "$CLAUDE_MD" | head -50)
echo "{\"inject\": \"Project rules (CLAUDE.md): $CONTENT\"}"
fi
exit 0Automatically verify the quality of results when Claude finishes a response.
{
"hooks": {
"Stop": [
{
"type": "prompt",
"prompt": "Review the task just completed. Check the following items: 1) Was the original request fully satisfied? 2) Is the newly written code free of obvious bugs? 3) Are there no TypeScript type errors? Respond 'no' if there are issues, or 'yes' if everything passes."
}
]
}
}Using this pattern, Claude self-reviews its work and automatically addresses any shortcomings.
Automatically handle repetitive permission approvals.
#!/bin/bash
# auto-permission.sh
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Auto-approve files inside the project
PROJECT_DIR="/home/user/my-project"
if [[ "$FILE_PATH" == "$PROJECT_DIR"* ]]; then
echo '{"decision": "allow"}'
exit 0
fi
# Block files outside the project
echo '{"decision": "deny", "reason": "Access to files outside the project has been blocked."}'
exit 0{
"hooks": {
"PermissionRequest": [
{
"type": "command",
"command": "/path/to/auto-permission.sh"
}
]
}
}Record the Hook execution process to a log file to diagnose issues.
{
"hooks": {
"PreToolUse": [
{
"type": "command",
"command": "cat | tee -a /tmp/claude-hooks.log | /path/to/hook-script.sh"
}
]
}
}When creating a new Hook, start with a simple echo to verify that the event fires correctly.
{
"hooks": {
"PreToolUse": [
{
"type": "command",
"command": "echo 'Test Hook executed' >> /tmp/hook-test.log"
}
]
}
}