Create deterministic automations that trigger at every Claude Code lifecycle event
Hooks are user-defined shell commands that execute at specific points in Claude Code's lifecycle. They provide deterministic control — ensuring certain actions always happen rather than relying on Claude to choose.
| Feature | Hooks | Skills | Subagents |
|---|---|---|---|
| When | Automatic lifecycle events | Invoked by Claude on-demand | Spawned for specific tasks |
| Control | Deterministic (always runs) | Optional (Claude decides) | Task-scoped |
| Types | Command, prompt, agent | Markdown instructions | Context-isolated sessions |
Configure hooks in any settings.json file, or use /hooks for an interactive setup wizard.
| Event | When It Fires |
|---|---|
SessionStart | Session begins, resumes, or after compaction |
Full access
Unlock all 14 lessons, templates, and resources for Claude Code Mastery. Free.
UserPromptSubmit |
| After you submit a prompt, before Claude processes |
PreToolUse | Before a tool call executes (can block it) |
PostToolUse | After a tool call succeeds |
PostToolUseFailure | After a tool call fails |
Notification | When Claude Code sends a notification |
SubagentStart | When a subagent is spawned |
SubagentStop | When a subagent finishes |
Stop | When Claude finishes responding |
TeammateIdle | When an agent team teammate goes idle |
TaskCompleted | When a task is marked complete |
PreCompact | Before context compaction |
SessionEnd | When a session terminates |
Q: What's the key difference between hooks and skills?
Hooks are deterministic — they always run when their event fires. Skills are optional — Claude decides whether to invoke them based on the task. Use hooks for things that must happen (formatting, blocking, notifications) and skills for things Claude should consider doing.
{
"hooks": {
"<EventName>": [
{
"matcher": "<tool_name_or_pattern>",
"hooks": [
{
"type": "command",
"command": "your-shell-command"
}
]
}
]
}
}
"" → matches all events"Bash" → matches that tool"Edit|Write" → matches either tool"compact" → matches compaction events (SessionStart)Every hook receives JSON on stdin with event-specific data:
{
"session_id": "abc123",
"cwd": "/Users/dev/project",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test"
}
}
| Exit Code | Meaning |
|---|---|
0 | Allow the action. stdout text → Claude's context |
2 | Block the action. stderr → Claude's feedback |
| Other | Allow, stderr logged (toggle verbose with Ctrl+O) |
This is crucial: Exit code
2is your power move. It lets hooks block tool calls and feed reasons back to Claude.
Q: A PreToolUse hook exits with code 2 and prints "Use rg instead of grep" to stderr. What happens?
The tool call is blocked. Claude receives the stderr message ("Use rg instead of grep") as feedback and will adjust its approach — likely switching to rg for the search.
Get alerted when Claude needs your input:
{
"hooks": {
"Notification": [{
"matcher": "",
"hooks": [{
"type": "command",
"command": "osascript -e 'display notification \"Claude needs attention\" with title \"Claude Code\"'"
}]
}]
}
}
Run Prettier on every file Claude edits:
{
"hooks": {
"PostToolUse": [{
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}]
}]
}
}
#!/bin/bash
# scripts/protect-files.sh
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
PROTECTED=(".env" "package-lock.json" ".git/")
for pattern in "${PROTECTED[@]}"; do
if [[ "$FILE" == *"$pattern"* ]]; then
echo "Blocked: $FILE is protected" >&2
exit 2
fi
done
exit 0
{
"hooks": {
"PreToolUse": [{
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"command": "./scripts/protect-files.sh"
}]
}]
}
}
{
"hooks": {
"SessionStart": [{
"matcher": "compact",
"hooks": [{
"type": "command",
"command": "echo 'Reminder: use Bun, not npm. Run bun test before committing. Current sprint: auth refactor.'"
}]
}]
}
}
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
if echo "$COMMAND" | grep -qE "drop table|truncate|DELETE FROM.*WHERE 1"; then
echo "Blocked: potentially destructive SQL operation" >&2
exit 2
fi
exit 0
Fill in the blanks:
___SessionStart with matcher "___"/___2compact/hooksRun a shell command. The most common type:
{
"type": "command",
"command": "echo 'Hello from hook'"
}
Use a Claude model for single-turn evaluation:
{
"type": "prompt",
"prompt": "Is this bash command safe to run? Respond with ALLOW or DENY and a reason."
}
Multi-turn verification with tool access:
{
"type": "agent",
"prompt": "Verify that the proposed changes don't break the API contract. Check the OpenAPI spec.",
"tools": ["Read", "Grep", "Glob"]
}
Q: When would you use an agent-based hook instead of a command hook?
Use agent-based hooks when verification requires AI reasoning and multi-step analysis — like checking whether code changes break an API contract or violate security policies. Agent hooks get their own tools (Read, Grep, etc.) and can do complex analysis. Use command hooks for deterministic actions like formatting, blocking known patterns, or sending notifications.
PreToolUse: any hook returning exit 2 blocks the actionCtrl+O to see hook execution logsHooks can persist environment variables using CLAUDE_ENV_FILE:
#!/bin/bash
# SessionStart hook that sets up environment
echo "DATABASE_URL=your-database-url-here" >> "$CLAUDE_ENV_FILE"
echo "NODE_ENV=development" >> "$CLAUDE_ENV_FILE"
Fill in the blanks:
Ctrl+___parallelpromptCtrl+O (verbose mode){
"hooks": {
"PostToolUse": [{
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"command": "jq -r '.tool_input.file_path' >> /tmp/claude-edits.log"
}]
}]
}
}
.claude/settings.local.json/tmp/claude-edits.log to see the logged pathREADME.mdReflection: How could you combine hooks to build an automated code quality pipeline?
Scenario: Your team runs a Python project where Claude keeps using pip instead of poetry. You want to automatically redirect it without manual intervention.
Create a PreToolUse hook that detects pip commands and blocks them with guidance:
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
if echo "$COMMAND" | grep -q "^pip "; then
echo "Blocked: Use 'poetry' instead of 'pip'. For example: poetry add <package>" >&2
exit 2
fi
exit 0
Claude receives the feedback and switches to poetry. This is deterministic — it works every time, unlike CLAUDE.md instructions which Claude might occasionally ignore.
| Concept | One-Liner |
|---|---|
| What hooks are | Deterministic shell commands at lifecycle events |
| Key events | SessionStart, PreToolUse, PostToolUse, Stop |
| Exit code 0 | Allow; stdout → Claude's context |
| Exit code 2 | Block; stderr → Claude's feedback |
| Matcher patterns | "" (all), "Bash" (tool), "Edit|Write" (multiple) |
| Three types | command, prompt, agent |
| Setup wizard | /hooks interactive command |
| Compaction hook | SessionStart with "compact" matcher |
Next up: Skills — Reusable Commands → — Build markdown-based playbooks that Claude follows on demand.