Skip to content

Hooks

A hook is a user-defined handler that runs automatically at a fixed point in Claude Code's lifecycle — think of it as an event listener for the agent loop.

The analogy that maps well for a software engineer: hooks are to Claude Code what middleware is to an Express app, or what Git hooks are to a repository. You're intercepting well-defined lifecycle events and injecting deterministic behavior — formatting, validation, blocking, logging — regardless of what the AI "decides" to do.

Hooks Are Deterministic

The key distinction from just prompting Claude is determinism. A prompt instruction like "always run Prettier after editing files" is advisory — Claude might forget or skip it. A PostToolUse hook that runs prettier --write on every file edit is guaranteed to execute every single time.

Hook Structure

Each hook has three parts:

  • Event — which lifecycle moment to intercept (PreToolUse, Stop, SessionStart, etc.)
  • Matcher — an optional regex filter to narrow when it fires (e.g. only on Bash tool calls, or only on Edit|Write operations)
  • Handler — what actually runs: a shell command, HTTP endpoint, LLM prompt eval, or a spawned subagent

An Example of a Hook

Here's a PostToolUse hook that runs Prettier after every file edit:

json
// .claude/settings.json
{
    "hooks": {
        "PostToolUse": [
            {
                "matcher": "Edit|Write|MultiEdit",
                "hooks": [
                    {
                        "type": "command",
                        "command": "npx prettier --write \"$CLAUDE_TOOL_INPUT_FILE_PATH\""
                    }
                ]
            }
        ]
    }
}

The matcher targets the three tools Claude uses to write files: Edit (single-block edits), MultiEdit (multiple edits in one call), and Write (full file writes). The env var $CLAUDE_TOOL_INPUT_FILE_PATH is injected by Claude Code and resolves to the file that was just modified.

Since PostToolUse is non-blocking, it can't undo the edit — but it runs synchronously before Claude's next action, so the file is formatted before Claude reads it back or makes further changes.

A few practical refinements worth considering:

Scope it to formattable files only, to avoid Prettier erroring on binary or unsupported types:

json
"command": "npx prettier --write \"$CLAUDE_TOOL_INPUT_FILE_PATH\" 2>/dev/null || true"

The || true ensures a Prettier failure (e.g. on a file type it doesn't handle) doesn't surface as a hook error.

Use a local Prettier config — Prettier will automatically pick up .prettierrc, prettier.config.js, etc. from the project root, so no extra config needed in the hook itself.

Prefer a local binary over npx if startup latency matters in a large project:

json
"command": "./node_modules/.bin/prettier --write \"$CLAUDE_TOOL_INPUT_FILE_PATH\" 2>/dev/null || true"

Some Hooks are Blocking

Some hooks may block the agent from continuing execution, and some never block. When a hook "blocks," it halts the operation that triggered it — the tool call, turn, or session event never proceeds (or is forced to continue, depending on the hook).

The mechanism is the exit code returned by the hook handler:

  • Exit 0 — hook ran fine, no decision, execution continues normally
  • Exit 2block: the operation is stopped, and stderr is sent to Claude as the reason
  • Any other code — error, shown to the user, but execution continues anyway

So "blocking" is just exit 2 in a shell script (or {"decision": "block"} in JSON output for HTTP/prompt hooks).

What "blocked" means depends on the hook:

HookWhat "block" does
PreToolUseTool call is cancelled; Claude sees the reason and can try something else
PermissionRequestPermission is denied without prompting the user
StopForces Claude to keep working instead of finishing the turn
SubagentStopSubagent output is rejected; it must continue
UserPromptSubmitPrompt is rejected before Claude ever sees it
WorktreeCreate/RemoveReplaces default git behavior entirely

The Stop hook is the most counterintuitive one — "blocking" a stop means preventing Claude from stopping, not stopping it from doing something. It's how you enforce task completion: if tests are still failing, exit 2 and Claude keeps going.

The Hooks

As of May 2026, there are 30 hook events over 9 categories, as shown below.

Session Lifecycle Hooks

Hook eventWhen it firesBlocks?Use Cases
SetupOn --init-only, --init, or --maintenance flags (CI/scripts)NoOne-time env bootstrapping; install deps; pre-flight checks in CI pipelines
SessionStartSession begins or resumes (after clear, compact, or resume)NoInject context from prior sessions; set env vars; load project-specific config; log session start to audit trail
SessionEndSession terminates for any reasonNoCleanup temp files; persist session summary to DB; send cost/usage report; flush logs

Per-turn Event Hooks

Hook eventWhen it firesBlocks?Use Cases
UserPromptSubmitUser hits enter — before Claude processes the promptYesAppend context (ticket ID, git branch) to every prompt; enforce prompt policies; validate input; log all user prompts
UserPromptExpansionA slash command expands into a prompt, before it reaches ClaudeYesValidate or gate custom slash commands; transform expansion output; block disallowed commands
StopClaude finishes responding (end of turn)YesEnforce task completion (force Claude to keep going if tests fail); run a final linter or type-check; send desktop notification when done
StopFailureTurn ends due to an API error (rate limit, billing, etc.)NoAlert on API errors; log billing/rate-limit events; trigger retry logic externally; send PagerDuty alert

Tool Execution Hooks

Hook eventWhen it firesBlocks?Use Cases
PreToolUseAfter Claude creates tool params, before the tool executesYesBlock destructive shell commands (rm -rf); auto-approve safe read ops; enforce allowlists; log all tool invocations; rewrite tool inputs
PermissionRequestA permission dialog appears (tool needs user approval)YesAuto-approve known-safe tools to eliminate click fatigue; auto-deny disallowed operations; enforce policy-based access control
PermissionDeniedA tool call is denied by the auto-mode classifierNoReturn {retry: true} to let Claude retry; log denied ops for security review; alert on repeated denials
PostToolUseAfter a tool call succeedsNoAuto-format files after write/edit; run linter after code changes; log tool results; send feedback to Claude for next iteration
PostToolUseFailureAfter a tool call failsNoLog errors to observability platform; alert on repeated failures; surface actionable hints back to Claude
PostToolBatchAfter a full batch of parallel tool calls resolves, before next model callNoAggregate results from parallel tool calls; run cross-file validation after a batch of edits; batch-log tool results

Subagent & Task Hooks

Hook eventWhen it firesBlocks?Use Cases
SubagentStartA subagent is spawned via the Agent tool or @-mentionNoInject subagent-specific context; log subagent spawns for orchestration tracing; initialize subagent env vars
SubagentStopA subagent finishes its workYesValidate subagent output before it's returned; enforce quality gates on delegated tasks; log subagent cost/duration
TaskCreatedA task is being created via TaskCreateNoLog task creation to external tracker; inject task context or constraints; audit task creation in multi-agent workflows
TaskCompletedA task is being marked as completedNoTrigger downstream automation on task completion; update external project boards; run acceptance checks
TeammateIdleAn agent-team teammate is about to go idleNoRe-assign work before a teammate idles; alert orchestrator; log idle events in parallel agent orchestration

Compaction & Context Hooks

Hook eventWhen it firesBlocks?Use Cases
PreCompactBefore context compaction runsNoBack up full transcript before it's summarized; extract and persist key decisions; snapshot current state
PostCompactAfter context compaction completesNoRe-inject project context after compaction; verify compaction summary accuracy; resume monitoring

MCP & Elicitation Hooks

Hook eventWhen it firesBlocks?Use Cases
ElicitationAn MCP server requests user input during a tool callNoLog elicitation events; surface custom UI for MCP prompts; route elicitation to specific channels (e.g. Slack)
ElicitationResultUser responds to an MCP elicitation, before response is sent to serverNoValidate or transform user elicitation responses; audit MCP interaction history; enforce response policies

File & Config Watching Hooks

Hook eventWhen it firesBlocks?Use Cases
InstructionsLoadedA CLAUDE.md or .claude/rules/*.md file is loaded into contextNoValidate instruction files on load; log which rules files were applied; dynamically inject supplemental instructions
ConfigChangeA configuration file changes during a live sessionNoHot-reload environment on config change; notify team of settings changes; validate new config before it takes effect
CwdChangedWorking directory changes (e.g. Claude runs cd)NoTrigger direnv reload; activate project-specific virtualenv; load per-directory .env; update shell prompt context
FileChangedA watched file changes on disk (matcher specifies filenames)NoRe-run build on source file change; reload config on .env change; trigger tests when a spec file is modified

Worktree Management Hooks

Hook eventWhen it firesBlocks?Use Cases
WorktreeCreateA worktree is being created via --worktree or isolation: "worktree"YesReplace default git worktree behavior; inject custom branch naming; provision worktree-specific resources
WorktreeRemoveA worktree is being removed (session exit or subagent finish)YesOverride git cleanup; archive worktree artifacts; deprovision worktree-specific infra before removal

Notification Hooks

Hook eventWhen it firesBlocks?Use Cases
NotificationClaude Code sends a notification (permission prompt, idle, auth, etc.)NoDesktop/TTS alerts when Claude needs input; route notifications to Slack; log all notification events

Controlling Agent Execution with Blocking Hooks

The following hooks are blocking hooks that establish a control plane.

  • PreToolUse
  • PermissionRequest
  • UserPromptSubmit
  • Stop
  • SubagentStop
  • WorktreeCreate
  • WorktreeRemove can all exit with code 2 to halt execution — everything else is observability/side-effects only.

Hook Handler Types

Four handler types exist (not just shell commands):

  • command — shell script (the classic)
  • http — POST to a local or remote endpoint (added Feb 2026); useful for centralized policy services
  • prompt — lightweight LLM evaluation, returns {ok: true/false}
  • agent — spawns a subagent with Read/Grep/Glob access for deeper codebase analysis

Notes

PostToolUse has a nuance: it can't block the tool that already ran, but it can write feedback to Claude's context, effectively steering the next turn.

async: true (added Jan 2026) lets you fire-and-forget any hook — good for logging, backups, and notifications that shouldn't add latency to the main loop.