Claude Code Hooks are the programmable seams of the agent loop. At every meaningful juncture — before a tool fires, after a file write, when a session starts, when a subagent finishes — you can intercept, validate, block, or enrich the context. This reference covers every lifecycle event shipped as of May 2026 (v2.1.141+), all five handler types, exit-code semantics, and production patterns that teams are actually running in CI/CD pipelines today.
If you’ve read the introductory material and want the authoritative cheat sheet, this is it.
What Are Claude Code Hooks
Hooks are event-driven callbacks that Claude Code calls during its agent loop. You register them in settings.json at user, project, or organization scope. When the matching event fires, Claude Code routes a JSON payload to your handler — a shell script, an HTTP endpoint, an MCP tool call, a prompt evaluation, or a subagent — and waits for a response that says proceed, block, or enrich.
The design philosophy is deliberate: hooks run synchronously in the agent loop (unless marked async). That synchronous contract is what gives them their power. A PreToolUse hook returning exit code 2 will prevent the tool from running, period. No workaround, no retry. This is the primitive that makes policy enforcement possible.
Three properties determine a hook’s behavior:
- Event — which lifecycle moment triggers it
- Matcher — which specific tool name, file pattern, or session type to filter on
- Handler — what executes when the event fires
The configuration lives in hooks inside settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/validate-bash.sh"
}
]
}
]
}
}
One event can have multiple matchers. Each matcher can have multiple hooks. They run in order.
The Complete Lifecycle Events Reference
Session Lifecycle
These events bracket the session itself — initialization, loaded configuration, and teardown.
| Event | When It Fires | Can Block | Handler Types |
|---|---|---|---|
SessionStart | Session begins or resumes | No | command, mcp_tool |
Setup | --init-only, --init, or --maintenance flag | No | command, mcp_tool |
SessionEnd | Session terminates | No | command, http, mcp_tool, prompt, agent |
InstructionsLoaded | CLAUDE.md or .claude/rules/*.md files loaded | No | all types |
SessionStart is the earliest hook point after a session is established. Use it to populate context, set a session title, or register file watchers. The matcher filters on how the session started: startup, resume, clear, or compact.
The output schema is unique — it accepts additionalContext, sessionTitle, and watchPaths:
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "Branch: feat/payments\nOpen PR: #412",
"sessionTitle": "payments-feature",
"watchPaths": ["/home/user/project/.env", "/home/user/project/config/"]
}
}
InstructionsLoaded fires after CLAUDE.md and any rules files are parsed. This is useful for validating rule file syntax or injecting supplemental context after the base instructions are set.
User Input Events
These fire between the user submitting a prompt and Claude processing it.
| Event | When It Fires | Can Block | Handler Types |
|---|---|---|---|
UserPromptSubmit | User submits a prompt | Yes | all types |
UserPromptExpansion | Slash command expands into a prompt | Yes | all types |
UserPromptSubmit is one of the most powerful hooks in the system. It intercepts every prompt before Claude sees it. You can block prohibited content, add project-specific context, or normalize the input.
Input payload:
{
"session_id": "abc123",
"cwd": "/home/user/project",
"permission_mode": "default",
"hook_event_name": "UserPromptSubmit",
"prompt": "Delete all the test fixtures"
}
To block: output a JSON object with "decision": "block" and "reason". To augment the prompt with context, use additionalContext:
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "Current sprint: Sprint 24. Affected service: payments-api."
}
}
UserPromptExpansion fires when a slash command (skill) expands. The matcher is the command name. This lets you gate which skills can run and under what conditions.
Tool Execution Events (The Agentic Loop)
This is the dense center of the hook system — six events wrapping every tool call.
| Event | When It Fires | Can Block | Handler Types |
|---|---|---|---|
PreToolUse | Before a tool call executes | Yes | all types |
PostToolUse | After a tool call succeeds | No | all types |
PostToolUseFailure | After a tool call fails | No | all types |
PostToolBatch | After a set of parallel tool calls complete | Yes | all types |
PermissionRequest | When a permission dialog would appear | Yes | all types |
PermissionDenied | When tool call denied in auto mode | No | all types |
PreToolUse is the primary enforcement point. The input includes tool_name and tool_input — the exact arguments Claude is about to pass. Your handler inspects the payload and either exits cleanly (proceed) or outputs a deny decision (block).
Standard input structure for PreToolUse:
{
"session_id": "abc123",
"cwd": "/home/user/project",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "rm -rf /tmp/old-build"
}
}
To block from a command hook, exit with code 2 and write a message to stderr. To block from JSON output, use the hookSpecificOutput schema:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Destructive commands require manual confirmation"
}
}
PostToolBatch fires after a group of parallel tool calls all complete. The Can Block flag means you can inject additional processing steps or fail the batch if any output is invalid.
PermissionRequest fires when Claude Code would normally show a permission dialog. Your hook can auto-approve, auto-deny, or pass through to the user. This is what teams use to build policy-as-code permission systems where the dialog never appears in CI.
Model and Response Events
These fire around Claude’s response generation.
| Event | When It Fires | Can Block | Handler Types |
|---|---|---|---|
Stop | Claude finishes responding | Yes | all types |
StopFailure | Turn ends due to API error | No | all types |
MessageDisplay | Assistant message text is displaying | No | all types |
Stop fires after every Claude turn completes. The Can Block flag here means you can review the output and inject additional instructions or prevent the session from naturally terminating. Production teams use this for quality gates: run automated tests after every code-writing turn and feed results back as context.
MessageDisplay fires while text is streaming. It cannot block, but it can observe — useful for logging, monitoring, or triggering side effects as Claude types.
Subagent and Team Events
These events handle multi-agent workflows.
| Event | When It Fires | Can Block | Handler Types |
|---|---|---|---|
SubagentStart | A subagent is spawned | No | all types |
SubagentStop | A subagent finishes | Yes | all types |
TeammateIdle | Agent team teammate goes idle | Yes | all types |
SubagentStart fires when the main agent creates a child agent. The matcher is the agent type — general-purpose, Explore, or any custom agent name. Use this to inject context specific to the subagent’s role.
SubagentStop fires when a subagent completes. Since it can block, this is where you implement review gates: a supervisor subagent that reads the completed agent’s output and either approves or requests a redo.
TeammateIdle is the Agent Teams coordination primitive. When a teammate finishes its current task and becomes available, this event fires on every other agent in the team. The matcher filters by teammate identity. This enables reactive work assignment — the orchestrator can immediately dispatch the next task.
Task Management Events
These events wrap the TaskCreate tool lifecycle.
| Event | When It Fires | Can Block | Handler Types |
|---|---|---|---|
TaskCreated | Task created via TaskCreate | Yes | all types |
TaskCompleted | Task marked as completed | Yes | all types |
TaskCreated lets you validate, normalize, or enrich tasks before they enter the queue. You can enforce naming conventions, add mandatory fields, or route tasks to specific agents.
TaskCompleted is the quality gate hook for task-based workflows. Before the task is marked done and the output is accepted, your hook can verify that the acceptance criteria are met.
File and Directory Events
These monitor the filesystem and working directory state.
| Event | When It Fires | Can Block | Handler Types |
|---|---|---|---|
FileChanged | A watched file changes on disk | No | all types |
CwdChanged | Working directory changes | No | all types |
WorktreeCreate | Worktree created via --worktree | Yes | all types |
WorktreeRemove | Worktree removed | No | all types |
FileChanged fires when a file registered in watchPaths (set via SessionStart hook output) changes. The classic use case is .env file monitoring — when environment variables change, reload or notify:
{
"hooks": {
"FileChanged": [
{
"matcher": ".envrc|.env",
"hooks": [
{
"type": "command",
"command": "direnv reload"
}
]
}
]
}
}
WorktreeCreate can block. If your project enforces branch naming conventions or requires a Jira ticket reference before opening a worktree, this is where you check.
Configuration Events
These fire around configuration changes and context compaction.
| Event | When It Fires | Can Block | Handler Types |
|---|---|---|---|
ConfigChange | Configuration file changes during session | Yes | all types |
PreCompact | Before context compaction | Yes | all types |
PostCompact | After context compaction completes | No | all types |
PreCompact fires before Claude compresses its context window. You can use this to save important state to disk before compaction discards it, or to block compaction under certain conditions (e.g., when a long transaction is in flight).
PostCompact fires after compaction completes. Reinject any critical context that you know compaction will drop.
MCP Server Events
These events wrap MCP server elicitation dialogs.
| Event | When It Fires | Can Block | Handler Types |
|---|---|---|---|
Elicitation | MCP server requests user input | Yes | all types |
ElicitationResult | After user responds to MCP elicitation | Yes | all types |
Elicitation is the programmatic answer to MCP form dialogs. When an MCP server needs user input (credentials, configuration choices, confirmation), your hook can supply answers automatically. The matcher is the MCP server name.
Output structure to auto-respond:
{
"hookSpecificOutput": {
"hookEventName": "Elicitation",
"action": "accept",
"content": {
"region": "us-east-1",
"environment": "staging"
}
}
}
Notification Events
| Event | When It Fires | Can Block | Handler Types |
|---|---|---|---|
Notification | Claude Code sends a notification | No | all types |
Notification fires for events like permission_prompt, auth_success, and elicitation_dialog. Use it to route notifications to your own alerting system rather than relying on desktop notifications.
Exit Codes and Flow Control
This is the most operationally important section. Getting exit codes wrong either silently fails your hook or crashes the agent loop in confusing ways.
| Exit Code | Meaning | Notes |
|---|---|---|
0 | Success — proceed normally | JSON output on stdout is processed |
2 | Blocking error — prevent the action | Stderr content shown to Claude |
Any other | Non-blocking error | Stderr goes to debug log only |
Exit code 2 is the block primitive. Any hook returning exit code 2 halts the action that triggered it. Claude sees the stderr content and can decide how to respond. This is intentionally transparent — Claude knows it was blocked and why.
Exit code 0 with JSON output is the enrichment path. Your hook proceeds but can return structured data that modifies Claude’s behavior:
{
"continue": true,
"suppressOutput": false,
"systemMessage": "Warning: deployment lock is active until 15:00 UTC",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Deployment window closed"
}
}
The systemMessage field injects a system-level message that Claude sees before deciding what to do next. Use it to explain policy decisions in terms Claude can reason about.
The suppressOutput flag prevents the hook’s stdout from appearing in the terminal UI — useful for hooks that write debug data you don’t want polluting the interface.
Hook Handler Types
Claude Code supports five distinct handler types. Choose based on where your logic lives and what latency is acceptable.
1. Command Hooks
The most common type. Executes a shell command with the event payload on stdin.
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/validate.sh",
"args": ["--strict"],
"shell": "bash",
"timeout": 60,
"statusMessage": "Validating...",
"async": false
}
Use the exec form (with args) for scripts that accept positional parameters. Use shell form (command string without args) for pipelines and one-liners.
The if field applies a permission rule filter — the hook only runs if the pattern matches:
{
"type": "command",
"if": "Bash(git *)",
"command": ".claude/hooks/git-validator.sh"
}
{
"type": "command",
"if": "Edit(*.ts)",
"command": ".claude/hooks/ts-lint.sh"
}
2. HTTP Hooks
Posts the event payload as JSON to an HTTP endpoint. Designed for sidecar services, audit APIs, and policy engines.
{
"type": "http",
"url": "https://policy.internal/hooks/pre-tool-use",
"headers": {
"Authorization": "Bearer $POLICY_API_TOKEN"
},
"allowedEnvVars": ["POLICY_API_TOKEN"],
"timeout": 30
}
Note allowedEnvVars — you must explicitly whitelist environment variables for interpolation. This prevents secrets from leaking via header injection.
The HTTP endpoint must return a JSON body matching the same schema as command hook stdout. Response status codes outside 2xx cause the hook to fail non-blockingly (exit code equivalent of non-zero, non-2).
3. MCP Tool Hooks
Calls a tool on a connected MCP server. Best when your validation logic is already exposed as an MCP capability.
{
"type": "mcp_tool",
"server": "security_scanner",
"tool": "scan_file",
"input": {
"file_path": "${tool_input.file_path}",
"severity_threshold": "high"
},
"timeout": 120
}
Template interpolation with ${...} is available for input values, pulling from the event payload fields.
4. Prompt Hooks
Sends the event context to a Claude model for single-turn evaluation. Use this when the decision logic is too nuanced for a script but you don’t need a full subagent.
{
"type": "prompt",
"prompt": "Evaluate this tool call. Return JSON with 'decision': 'allow' or 'deny' and 'reason'. Tool: $ARGUMENTS",
"model": "claude-opus-4",
"timeout": 30
}
The $ARGUMENTS variable expands to a JSON-serialized summary of the event payload. Prompt hooks are slower than command hooks and consume API tokens, but they can handle ambiguous cases that rule-based scripts can’t.
5. Agent Hooks
Spawns a full subagent to evaluate the event and return a decision. The most powerful and most expensive option.
{
"type": "agent",
"prompt": "Review this deployment operation. Check the change against the deployment runbook at .claude/runbooks/deploy.md and confirm it's safe to proceed. $ARGUMENTS",
"timeout": 120
}
Agent hooks are appropriate for Stop and SubagentStop events where you want a full code review or test run before accepting the output.
Real-World Patterns
Pattern 1: Destructive Command Firewall
Block rm -rf and other destructive shell patterns before they execute. The hook reads the command from tool_input.command, pattern-matches, and either exits clean or blocks with exit code 2.
#!/bin/bash
# .claude/hooks/bash-firewall.sh
COMMAND=$(cat /dev/stdin | jq -r '.tool_input.command // ""')
BLOCKED_PATTERNS=(
"rm -rf /"
"rm -rf ~"
"dd if=/dev/zero"
"mkfs"
"> /etc/passwd"
)
for pattern in "${BLOCKED_PATTERNS[@]}"; do
if echo "$COMMAND" | grep -qF "$pattern"; then
echo "BLOCKED: '$pattern' is a prohibited command pattern" >&2
exit 2
fi
done
exit 0
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/bash-firewall.sh",
"statusMessage": "Checking command safety..."
}
]
}
]
}
}
Pattern 2: Auto-Lint After File Writes
Run ESLint immediately after any TypeScript or JavaScript file is written. If lint fails, the failure surfaces as a system message that Claude sees and can fix in the same turn.
#!/bin/bash
# .claude/hooks/post-write-lint.sh
FILE_PATH=$(cat /dev/stdin | jq -r '.tool_input.file_path // ""')
if [[ "$FILE_PATH" =~ \.(ts|tsx|js|jsx)$ ]]; then
LINT_OUTPUT=$(npx eslint --no-eslintrc -c .eslintrc.json "$FILE_PATH" 2>&1)
LINT_EXIT=$?
if [ $LINT_EXIT -ne 0 ]; then
# Return as systemMessage so Claude sees it immediately
jq -n --arg msg "ESLint found issues in $FILE_PATH:\n$LINT_OUTPUT" \
'{systemMessage: $msg}'
exit 0
fi
fi
exit 0
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/post-write-lint.sh",
"statusMessage": "Linting..."
}
]
}
]
}
}
Pattern 3: Session Context Loader
Inject branch, PR status, and recent commit log into every new session automatically. Claude has full project context from turn one without requiring the user to paste it.
#!/bin/bash
# .claude/hooks/session-context.sh
BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
RECENT_COMMITS=$(git log --oneline -5 2>/dev/null || echo "no git history")
OPEN_PR=$(gh pr list --head "$BRANCH" --json number,title --jq '.[0] | "#\(.number) \(.title)"' 2>/dev/null || echo "none")
CONTEXT="Git branch: $BRANCH
Open PR: $OPEN_PR
Recent commits:
$RECENT_COMMITS"
jq -n \
--arg ctx "$CONTEXT" \
--arg branch "$BRANCH" \
'{
hookSpecificOutput: {
hookEventName: "SessionStart",
additionalContext: $ctx,
sessionTitle: $branch,
watchPaths: [".env", ".envrc", "package.json"]
}
}'
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/session-context.sh"
}
]
}
]
}
}
Pattern 4: CI/CD Pre-Commit Gate via HTTP Hook
In a team environment, route every Edit and Write event to a central policy service that checks against organization-wide rules. The service can enforce things like “no hardcoded credentials”, “all API routes need auth middleware”, and “migration files require a JIRA ticket”.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "http",
"url": "https://policy.company.internal/claude-hooks/pre-tool-use",
"headers": {
"Authorization": "Bearer $CLAUDE_POLICY_TOKEN",
"X-Project": "payments-api",
"X-Environment": "development"
},
"allowedEnvVars": ["CLAUDE_POLICY_TOKEN"],
"timeout": 10,
"statusMessage": "Policy check..."
}
]
}
]
}
}
The policy service reads the tool_input.file_path and tool_input.content fields, runs its checks, and returns either a clean JSON {} or a permissionDecision: "deny" response. Development teams get consistent guardrails without maintaining per-project hook scripts.
Pattern 5: Automated Test Run on Stop
After every Claude turn that modifies source files, run the relevant test suite. The Stop hook fires at turn end; if tests fail, inject the output as a system message so Claude’s next turn includes the failure context.
#!/bin/bash
# .claude/hooks/run-tests-on-stop.sh
# Only run if Claude wrote files this turn (check transcript)
TRANSCRIPT=$(cat /dev/stdin | jq -r '.transcript_path // ""')
WROTE_FILES=$(tail -50 "$TRANSCRIPT" 2>/dev/null | jq -r 'select(.type=="tool_result" and .tool_name=="Write") | .tool_input.file_path' 2>/dev/null | head -1)
if [ -z "$WROTE_FILES" ]; then
exit 0
fi
TEST_OUTPUT=$(npm test -- --passWithNoTests 2>&1 | tail -30)
TEST_EXIT=$?
if [ $TEST_EXIT -ne 0 ]; then
jq -n --arg output "$TEST_OUTPUT" \
'{systemMessage: ("Tests failed after last edit:\n" + $output)}'
fi
exit 0
Agent Teams Integration
Agent Teams is Claude Code’s multi-agent coordination layer. Hooks integrate deeply with it through three events.
TeammateIdle
When a teammate finishes its current task and returns to idle, TeammateIdle fires on every other agent in the team. The orchestrator agent typically handles this event to dispatch the next pending task.
{
"hooks": {
"TeammateIdle": [
{
"matcher": "reviewer",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/dispatch-next-task.sh",
"statusMessage": "Reviewer available — dispatching next PR..."
}
]
}
]
}
}
The matcher is the teammate’s agent type string. If the reviewer agent goes idle, check the task queue and immediately assign the next pending code review.
TaskCreated and TaskCompleted
These events give you lifecycle control over the TaskCreate tool that agents use to create and track work items.
TaskCreated fires before the task enters the queue. Use it to enforce structure — task descriptions must include acceptance criteria, priority must be set, affected service must be named:
#!/bin/bash
# .claude/hooks/validate-task.sh
TASK=$(cat /dev/stdin)
TITLE=$(echo "$TASK" | jq -r '.tool_input.title // ""')
DESCRIPTION=$(echo "$TASK" | jq -r '.tool_input.description // ""')
if [ ${#DESCRIPTION} -lt 50 ]; then
echo "Task description too short. Include acceptance criteria (min 50 chars)." >&2
exit 2
fi
if ! echo "$TITLE" | grep -qP '^\[(FEAT|BUG|CHORE|DOCS)\]'; then
echo "Task title must start with [FEAT], [BUG], [CHORE], or [DOCS]." >&2
exit 2
fi
exit 0
TaskCompleted fires before the completion is accepted. Use a SubagentStop-style reviewer agent to verify the work meets the acceptance criteria before marking done.
Security Considerations
Hooks run on your local machine with your user’s permissions. A compromised hook is equivalent to a compromised shell session. A few principles:
Validate all inputs. Hook scripts receive JSON from the Claude Code process. Treat every field as potentially tainted. Use jq for parsing — never eval or string interpolation of hook payload values into shell commands.
# WRONG — arbitrary command injection via tool_input
COMMAND=$(cat /dev/stdin | grep '"command"' | cut -d'"' -f4)
eval "$COMMAND" # never do this
# RIGHT — parse with jq, use as data only
COMMAND=$(cat /dev/stdin | jq -r '.tool_input.command // ""')
if echo "$COMMAND" | grep -qE '^(ls|pwd|git status)$'; then
exit 0
fi
Avoid HTTP hooks to public endpoints for sensitive payloads. The PreToolUse payload contains the full content of file writes. Routing this to an external service leaks your code. Keep HTTP hooks on internal network endpoints or use TLS with certificate pinning for any external service.
Use allowedEnvVars conservatively. Only list the specific environment variables your hook needs. Never use a wildcard pattern.
Scope hooks to the least powerful handler type. A command hook that runs a 10-line bash validator is lower risk than a prompt hook that sends full file content to an LLM for evaluation. Match the handler to the actual complexity of the decision.
Audit disableAllHooks. Any settings file can include "disableAllHooks": true to bypass all hooks. In team environments, managed policy settings can prevent this override — configure organization-level hooks in managed settings so developers cannot bypass security hooks.
{
"disableAllHooks": false
}
Managed settings (set by your org admin) override user and project settings. If your security hooks are in managed settings, disableAllHooks in a project settings file has no effect on them.
Debugging and Troubleshooting
Inspect Active Hooks
Type /hooks in any Claude Code session to open a read-only browser showing all configured hooks, organized by event, matcher, and handler. This is the fastest way to confirm your hook is registered and see its full configuration.
Enable Debug Logging
Non-zero, non-2 exit codes log to the debug output. Run Claude Code with verbose logging to see them:
claude --debug
The debug log shows which hooks fired, their exit codes, and any stderr output that would otherwise be silently swallowed.
Test Hook Scripts Directly
Because hooks receive JSON on stdin, you can test them without running Claude Code at all:
# Simulate a PreToolUse event for a Bash tool call
echo '{
"session_id": "test",
"cwd": "/tmp",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "rm -rf /tmp/build"
}
}' | bash .claude/hooks/bash-firewall.sh
echo "Exit code: $?"
This makes hook scripts fully unit-testable. Write tests for your firewall scripts before deploying them to production sessions.
Common Issues
Hook fires but doesn’t block: Check exit code. A hook must exit with code exactly 2 to block. Exit code 1 is a non-blocking error — the action proceeds and the error goes to debug log only.
Hook not firing at all: Verify the event name spelling (case-sensitive). Check that the matcher pattern matches the tool name exactly. Use /hooks to confirm the hook is registered.
JSON output not being processed: JSON must go to stdout. Debug messages go to stderr. If your script mixes them, the JSON parser may fail silently. Use >&2 for all diagnostic output.
Async hooks not completing: If using "async": true, hooks don’t block the agent loop. If you need the result before the action proceeds, remove async: true.
Timeout issues: Default timeout is 600 seconds. If your hook calls an external service with high latency, reduce the timeout to fail fast rather than blocking the agent loop indefinitely.
FAQ
Can hooks modify the tool call arguments before execution?
No — PreToolUse hooks can block or allow, but they cannot rewrite the arguments Claude is about to pass to a tool. If you need different arguments, the block path is your option: block the call, inject a system message explaining why, and let Claude reformulate the tool call.
What’s the difference between PostToolUse and Stop?
PostToolUse fires after each individual tool call. Stop fires after Claude’s entire response turn is complete (which may involve many tool calls). Use PostToolUse for per-file linting; use Stop for end-of-turn test runs.
Can I use hooks in a skill or agent YAML frontmatter?
Yes. Hooks declared in skill or agent frontmatter are scoped to that component’s lifetime. A PreToolUse hook in a skill YAML only runs while that skill is active. The once: true field is also available in component-scoped hooks to run a setup hook exactly once per session.
Are prompt hooks and agent hooks counted against my API token usage?
Yes. Each prompt hook invocation makes an API call to the specified model. agent hooks spawn a subagent, consuming tokens for the subagent’s context and response. For high-frequency events like PreToolUse, prefer command hooks or HTTP hooks to avoid unexpected token consumption.
How do managed settings interact with project settings?
Managed settings take precedence over project and user settings. Hooks defined in managed settings cannot be overridden or disabled by project-level configuration. This is the mechanism for organization-wide security policies that individual developers cannot bypass.
Can a hook add new tools to the session?
Not directly. Hooks cannot modify the tool set. However, a SessionStart hook can inject additionalContext that describes what tools are available, and hooks can call MCP tools (via mcp_tool type) that may themselves have broader capabilities.
What happens if two hooks both try to block the same event?
The first hook that returns exit code 2 wins. Subsequent hooks in the same event/matcher group are not evaluated after a block. Hooks in different matcher groups under the same event all run, but any one of them can block.