Hooks & Lifecycle Automation
12 min read
What You’ll Learn
This chapter walks you through Claude Code’s hook system, which lets you automate actions at specific points in the tool’s lifecycle. We’ll cover all 18 lifecycle events (split across three categories), the four handler types you can use to respond to events, configuration format and matcher patterns, exit code semantics for controlling execution flow, the hookSpecificOutput schema for PreToolUse decision control, and how to manage hooks interactively.
By the end, you’ll be able to configure hooks that lint code before commits, block dangerous commands, send notifications on task completion, and enforce project-specific workflows.
What Are Hooks?
Hooks are shell commands that fire automatically at specific points in the Claude Code lifecycle, enabling CI-style automation directly within the agentic workflow.
Hooks are automations that run at specific points in Claude Code’s lifecycle. They fire in response to events such as a session starting, a tool about to run, or a prompt being submitted, and can do just about anything: run a shell script, call an HTTP endpoint, evaluate an LLM prompt, or hand things off to a subagent.
Think of them as CI style automation baked right into your Claude Code workflow. Instead of waiting for a push to kick off a pipeline, hooks trigger the moment the event happens. You can lint code before Claude commits it, block destructive shell commands before they run, log every tool invocation to an audit system, or ping your team when Claude wraps up a task.
You configure hooks in settings files (user, project, or managed), and they apply to all sessions within their scope. You can also embed hooks in skill and subagent frontmatter for skill scoped automation; see Custom Skills for that pattern.
Hook Lifecycle Overview
Claude Code fires 18 lifecycle events across three categories: session events that bookend the overall session, loop events that fire during the agentic loop, and standalone async events that fire independently of the main loop.
The diagram above shows the full lifecycle flow. Session events (SessionStart and SessionEnd) wrap the entire interaction. In between, the agentic loop fires events as Claude gathers context, takes actions, and verifies results. Standalone async events fire independently whenever configuration changes, instructions load, or worktrees get created.
Getting a feel for which events fire and in what order is the foundation for writing effective hooks. The next three sections break down each category.
Here’s a quick look at hooks in action, a SessionStart hook fires at startup, and PreToolUse hooks fire before each tool call:
View as text (accessible transcript)
$ claude
🔒 Session hook active. All tool calls will be logged
> Check if there are any TODO comments in the codebase
✓ Hook approved: Grep
Searched for TODO patterns...
✓ Hook approved: Read
Read src/routes/auth.ts
Found 3 TODO comments:
- src/routes/auth.ts: TODO: hash password before storing
- src/middleware/auth.ts: TODO: load public paths from config
- src/index.ts: TODO: add rate limiting middlewareSession Events
Session events fire once at the start and end of each Claude Code session.
SessionStart fires when Claude Code initializes a new session, before any user prompt gets processed. It’s great for environment setup: checking prerequisites, loading environment variables, starting background services, or recording the session start time for metrics.
{ "hooks": { "SessionStart": [ { "hooks": [ { "type": "command", "command": "echo "Session started at $(date)" >> ~/.claude/session.log" } ] } ] }}A SessionStart hook that logs when each session begins.
SessionEnd fires when the session is winding down, after the final response. It’s your chance to clean up: stop background services, upload session logs, or send a summary notification.
Loop Events
Loop events fire during the agentic loop as Claude processes prompts and uses tools. There are 12 of them in total. Five are critical for most hook workflows; the remaining seven handle more specialized needs.
Key Loop Events
UserPromptSubmit fires when the user submits a prompt, before Claude starts processing it. It’s useful for prompt preprocessing, input validation, or logging user activity. This event fires for every prompt in a session, including follow up messages.
PreToolUse fires right before Claude executes a tool. This is the only loop event that can block execution. When a PreToolUse hook returns exit code 2 (or a deny decision via hookSpecificOutput), the tool call gets blocked and Claude receives feedback explaining why. That makes PreToolUse your go to event for safety gates: blocking destructive commands, enforcing file access rules, or requiring approval for sensitive operations.
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": ".claude/hooks/validate-command.sh" } ] } ] }}A PreToolUse hook that validates Bash commands before execution. Only PreToolUse can block.
PostToolUse fires after a tool finishes executing. It’s handy for post processing: logging tool results, validating outputs, or triggering follow up actions. Keep in mind that PostToolUse can’t block anything, since it runs after the fact.
Stop fires when Claude decides it’s done with the task and is about to exit the agentic loop. Use it for quality checks: running a final lint pass, verifying test coverage, or confirming the output meets your requirements.
PreCompact fires before Claude auto-compacts the conversation to free up context window space. You can use it to inject important context that should survive compaction, or to save a snapshot of the conversation state before information gets compressed.
Additional Loop Events
The remaining seven loop events cover more specialized use cases. Most hook configurations won’t need them.
| Event | When It Fires | Can Block |
|---|---|---|
| PermissionRequest | Claude asks the user for tool permission | No |
| PostToolUseFailure | A tool execution fails with an error | No |
| Notification | Claude sends a notification (e.g., task progress) | No |
| SubagentStart | A subagent is spawned | No |
| SubagentStop | A subagent completes its work | No |
| TeammateIdle | A teammate in an agent team becomes idle | No |
| TaskCompleted | A task in an agent team is marked complete | No |
Notice that PreToolUse is the only loop event in the “Can Block” column. Every other loop event is purely observational. They let you react to what happened, but they can’t prevent it.
Standalone Async Events
Four events fire independently of the main agentic loop. They respond to changes in Claude Code’s configuration and workspace rather than to user prompts or tool calls.
InstructionsLoaded fires when Claude Code loads instruction files (CLAUDE.md, settings.json, etc.) at session startup or whenever those files change during a session. You can use it to validate instructions or log which configuration files are active.
ConfigChange fires when a settings file gets modified during a session. It’s useful for reloading custom configuration, notifying the user about setting changes, or validating that new settings are compatible.
WorktreeCreate fires when a new git worktree is created (via the —worktree flag or subagent isolation). It’s your opportunity to set up the new worktree environment: installing dependencies, copying configuration files, or initializing databases.
WorktreeRemove fires when a git worktree is removed. Use it for cleanup: removing temporary files, closing database connections, or archiving worktree logs.
Explore Events Interactively
Click any event below to see its payload fields, handler types, and configuration examples.
Configuration Format
Hook configuration follows a three level nesting structure: the event name 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": ".claude/hooks/validate-bash.sh" } ] }, { "matcher": "Write", "hooks": [ { "type": "command", "command": ".claude/hooks/validate-write.sh" } ] } ], "SessionStart": [ { "hooks": [ { "type": "command", "command": ".claude/hooks/setup-env.sh" } ] } ] }}Two PreToolUse matcher groups (Bash and Write) and one SessionStart hook without a matcher.
Here’s how the three levels break down:
- Event name (e.g.,
PreToolUse,SessionStart), which determines the lifecycle event that triggers the hooks. - Matcher group (the objects in the event’s array), which filters which specific instances of the event should trigger this group’s hooks. If there’s no
matcherfield, the hooks run on every instance of the event. - Hook handlers (the objects in the
hooksarray), the actual automations that run when the event fires and the matcher matches.
You can configure hooks at three scopes:
- User settings (
~/.claude/settings.json): hooks apply to all your sessions on this machine. - Project settings (
.claude/settings.json): hooks apply to all sessions in this project. Commit them to Git for team-wide hooks. - Managed settings: hooks enforced by organization administrators. Users can’t override these.
Matcher Patterns
Matchers filter which instances of an event trigger the associated hooks. Different event types support different matcher fields.
PreToolUse and PostToolUse match on the tool name. The matcher value is a string, either an exact tool name or a regular expression. So when Claude is about to use the Bash tool, a matcher of "Bash" fires. A matcher of "Bash|Write" fires for both Bash and Write tool calls.
{ "hooks": { "PreToolUse": [ { "matcher": "Bash|Write", "hooks": [ { "type": "command", "command": ".claude/hooks/audit-changes.sh" } ] } ] }}This hook fires before both Bash and Write tool calls.
When you don’t specify a matcher, the hooks fire for every instance of the event. For session events (SessionStart, SessionEnd), matchers are typically omitted since these events only fire once per session.
Handler Types
Each hook handler specifies a type that determines how the automation runs. Claude Code supports four handler types.
command
The command handler runs a shell script or command, and it’s by far the most common handler type. The command receives event data as JSON on stdin and can output JSON to stdout to influence Claude Code’s behavior.
#!/bin/bash# .claude/hooks/validate-bash.sh# Reads the tool input from stdin, checks for dangerous patterns
INPUT=$(cat)COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if echo "$COMMAND" | grep -qE 'rms+-rfs+/'; then# Output JSON to deny the commandecho '{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "Blocked: rm -rf / is not allowed" }}'exit 2 # Blocking errorfi
# Command is safe, allow itexit 0A command handler that blocks rm -rf / via exit code 2 and hookSpecificOutput.
HTTP
The HTTP handler sends a POST request to a URL with the event data as the JSON body. It’s ideal for integrating with external systems: logging to a webhook, notifying a Slack channel, or calling an internal API.
{ "type": "http", "url": "https://hooks.example.com/claude-events"}Sends event data as a POST request to the specified URL.
Worth noting: enterprise administrators can restrict which URLs are allowed for HTTP hooks using the allowedHttpHookUrls setting in managed configuration. This prevents hooks from sending data to unauthorized endpoints.
prompt
The prompt handler runs an LLM prompt with the event context. Claude evaluates the prompt and can return a decision or analysis. It’s perfect for nuanced checks that need reasoning beyond simple pattern matching, such as evaluating whether a code change follows project conventions.
{ "type": "prompt", "prompt": "Review the following tool call. If the command could delete important data or modify system files, respond with a JSON object containing hookSpecificOutput with permissionDecision set to 'ask'. Otherwise respond with an empty JSON object {}."}An LLM evaluates the event and returns a decision.
agent
The agent handler delegates the event to a subagent for more complex analysis. The subagent runs in its own context and can use tools to investigate before returning a response. It’s the right choice for hooks that need multi-step reasoning or file analysis.
Exit Code Semantics
For command handlers, the exit code tells Claude Code how to interpret the result. There are three categories that carry specific meaning.
Exit code 0 means success. The hook ran without issues and Claude Code continues normally. Use exit 0 when your hook’s checks pass or when there’s nothing to act on.
Exit code 2 means blocking error. Claude Code stops the current operation. This only matters for PreToolUse hooks, since that’s the only event that can block. When a PreToolUse hook exits with code 2, the tool call gets prevented and Claude receives the hook’s output as feedback explaining why. For non-blocking events, exit code 2 is treated the same as any other non-zero code.
Any other exit code (1, 3, 4, etc.) means non blocking error. Claude Code logs a warning but keeps going. The hook’s failure doesn’t stop the operation. Use non zero exit codes for hooks that encounter errors but shouldn’t block Claude’s work, such as a logging hook that can’t reach its endpoint.
#!/bin/bash# .claude/hooks/block-rm.sh# Block rm -rf commands via PreToolUse
INPUT=$(cat)COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if echo "$COMMAND" | grep -qE 'rms+-rf'; thenecho '{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "Destructive rm -rf command blocked by safety hook" }}'exit 2 # BLOCKS the tool callfi
exit 0 # Allows the tool callExit 2 blocks the Bash tool call. Exit 0 allows it. Any other code warns but allows.
JSON Output and Decision Control
Command handlers can output JSON to stdout to communicate decisions and metadata back to Claude Code. The JSON output schema supports five top level fields.
continue is a boolean that controls whether Claude should keep processing. Set it to false to stop the agentic loop after this event.
stopReason is a string explaining why processing should stop. Only relevant when continue is false.
suppressOutput is a boolean that prevents the hook’s output from showing up in Claude’s context. Handy for hooks that log externally and don’t need Claude to see the result.
systemMessage is a string injected as a system message into Claude’s context. Use it to provide feedback, warnings, or instructions that Claude should factor into its next step.
hookSpecificOutput is an object with event specific fields. For PreToolUse, this is the primary mechanism for decision control.
For PreToolUse events, hookSpecificOutput supports three decision values:
- permissionDecision: “allow” automatically approves the tool call without asking the user.
- permissionDecision: “deny” blocks the tool call. Claude receives the reason and adjusts its approach.
- permissionDecision: “ask” presents the tool call to the user for manual approval.
The permissionDecisionReason field provides an explanation that Claude (or the user, in the case of “ask” decisions) sees alongside the decision.
#!/bin/bash# .claude/hooks/enforce-write-rules.sh# Enforce file write restrictions via PreToolUse
INPUT=$(cat)FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Block writes to production configif echo "$FILE_PATH" | grep -qE '^config/production'; thenecho '{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "Production config files are read only. Use staging config instead." }}'exit 2fi
# Auto-approve writes to test filesif echo "$FILE_PATH" | grep -qE '.test.(ts|js)$'; thenecho '{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "allow", "permissionDecisionReason": "Test files are auto-approved" }}'exit 0fi
# Everything else goes through normal permission flowexit 0Deny writes to production config, auto-approve test files, default flow for everything else.
A quick note: the older top level decision and reason fields are deprecated. Always use hookSpecificOutput with permissionDecision and permissionDecisionReason for PreToolUse decision control.
Here’s a PreToolUse blocking hook in action. Claude tries to run a destructive command, the hook blocks it, and Claude adapts with a safer alternative:
View as text (accessible transcript)
$ cat .claude/settings.json
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": ".claude/hooks/block-rm.sh"
}]
}]
}
}
$ claude
> Clean up all temporary files by running rm -rf /tmp/my-app-cache
BLOCKED: Refusing to run destructive rm command.
Claude adapts and suggests a safer alternative.
> List all TypeScript files in the project
This command runs normally. The hook only blocks rm -rf.Hook Locations and Scopes
Hooks can be defined in six locations. Unlike most settings where later scopes override earlier ones, hooks from all locations merge and all run. If you have a PreToolUse hook in user settings and another in project settings, both execute, and neither overrides the other.
User settings (~/.claude/settings.json): your personal hooks across all projects.
Project settings (.claude/settings.json): team hooks shared via Git.
Local settings (.claude/settings.local.json): your personal hooks for this project only. Not committed to Git.
Managed settings: organization enforced hooks distributed by administrators. Users can’t remove these.
Plugin hooks: hooks provided by installed plugins.
Skill and agent frontmatter: hooks embedded in SKILL.md or AGENT.md hooks field. These only run when that specific skill or agent is active. See Custom Skills for the frontmatter format.
This merging behavior means you can layer hooks: organization-wide safety rules in managed settings, team specific workflow hooks in project settings, and personal productivity hooks in user settings. All three layers run for every event, giving you defense in depth.
Managing Hooks with /hooks
The /hooks slash command gives you an interactive menu for managing hooks without manually editing JSON files. It shows all configured hooks across scopes, lets you add new hooks with guided prompts, and supports deleting hooks you no longer need.
/hooksView, add, and delete hooks from all scopes in an interactive menu.
The /hooks menu is the fastest way to experiment with hooks. Create one interactively, test it, and then move the configuration to a settings file once you’re happy with it. It’s especially useful for debugging, since you can see exactly which hooks are active and which scope they’re coming from.
Best Practices
-
Use PreToolUse for safety gates. It’s the only event that can block execution. Put your most important guardrails here: blocking destructive commands, enforcing file access rules, and requiring approval for sensitive operations.
-
Keep hook scripts fast. Hooks run synchronously and block Claude Code while they execute. A slow hook that takes 5 seconds adds 5 seconds of latency to every matching event. Optimize for speed: avoid network calls in command handlers when possible, and lean on HTTP handlers for async integrations.
-
Use exit codes correctly. Exit 0 for success, exit 2 for blocking errors (PreToolUse only), and any other code for non blocking warnings. Getting this wrong can either block operations you meant to allow or silently pass operations you meant to block.
-
Test hooks in isolation before deploying. Pipe sample JSON into your hook script and verify the output before adding it to settings. A broken hook can disrupt every session.
-
Use matchers to narrow scope. A PreToolUse hook without a matcher fires for every tool call, including Read, Grep, Glob, Bash, Write, and more. Matchers let you target only the tools that actually need validation.
-
Prefer hookSpecificOutput over deprecated formats. The top level
decisionandreasonfields still work but are deprecated. Stick withhookSpecificOutputusingpermissionDecisionandpermissionDecisionReasonfor PreToolUse decision control. -
Layer hooks across scopes for defense in depth. Organization safety rules in managed settings, team workflow hooks in project settings, and personal productivity hooks in user settings all merge and run together.
Further Reading
- Hooks, the official documentation on lifecycle events, configuration format, handler types, and decision control
- Next chapter: Git Worktrees & Subagent Delegation covers parallel development branches and custom agent personas