What Are Claude Code Hooks?
Hooks are Claude Code's official extension mechanism. They let you run custom code at specific points in the agent's lifecycle: before a tool executes, after a file is written, when a permission dialog appears, or when the agent finishes responding.
Hooks are defined in JSON configuration files (.claude/settings.json for project-level, ~/.claude/settings.json for user-level) and can be shell commands, HTTP endpoints, LLM prompts, or subagents. When an event fires, Claude Code passes JSON context to your hook via stdin (for commands) or as a POST body (for HTTP hooks). Your hook can inspect the data, take action, and optionally return a decision that controls whether the agent proceeds.
This guide covers every hook event available in Claude Code as of March 2026, with practical examples for each.
Hook Configuration Structure
Every hook follows the same three-level structure:
- Event -- which lifecycle point to respond to
- Matcher -- a regex filter for when the hook should fire
- Handler -- the command, URL, prompt, or agent to execute
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/validate-bash.sh"
}
]
}
]
}
}
The matcher is a regex string. "Bash" matches only the Bash tool. "Edit|Write" matches either tool. "mcp__.*" matches all MCP tools. Omit the matcher or use "*" to match everything.
The Complete Hook Events Reference
Claude Code provides 17 hook events. They fire in a specific order during a session's lifecycle. Here is every one of them.
Session Lifecycle Events
These events fire once per session (or once per lifecycle transition), not inside the agentic loop.
SessionStart
When it fires: When a new session begins or an existing session is resumed.
Matcher values: startup, resume, clear, compact
Use cases: Loading project context, setting environment variables, initializing monitoring.
Special capability: SessionStart hooks can persist environment variables via the CLAUDE_ENV_FILE mechanism. Write export statements to this file, and they become available in all subsequent Bash commands during the session.
#!/bin/bash
# .claude/hooks/session-init.sh
# Load project-specific environment on session start
if [ -n "$CLAUDE_ENV_FILE" ]; then
echo 'export NODE_ENV=development' >> "$CLAUDE_ENV_FILE"
echo 'export DEBUG=app:*' >> "$CLAUDE_ENV_FILE"
fi
echo '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"Project uses pnpm. Always use pnpm instead of npm."}}'
How CodePulse uses it: CodePulse registers a SessionStart hook to initialize the Telegram connection, load auto-approval patterns, and notify you that a new session has begun.
SessionEnd
When it fires: When the session terminates.
Matcher values: clear, logout, prompt_input_exit, bypass_permissions_disabled, other
Use cases: Cleanup tasks, logging session statistics, archiving data.
Note: SessionEnd hooks cannot block session termination. They run for side effects only.
#!/bin/bash
# .claude/hooks/session-end.sh
INPUT=$(cat)
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id')
echo "Session $SESSION_ID ended at $(date)" >> ~/.claude/session-log.txt
How CodePulse uses it: CodePulse logs final session statistics (total tokens, duration, events processed) and sends a session summary to Telegram.
Prompt Events
UserPromptSubmit
When it fires: After you submit a prompt, before Claude processes it.
Matcher: No matcher support. Fires on every prompt.
Use cases: Prompt validation, injecting additional context, blocking certain prompts.
Decision control: Return {"decision": "block", "reason": "..."} to reject the prompt. Print plain text to stdout to add context that Claude sees.
#!/bin/bash
# .claude/hooks/add-context.sh
# Add the current git branch as context for every prompt
BRANCH=$(git branch --show-current 2>/dev/null)
if [ -n "$BRANCH" ]; then
echo "Current git branch: $BRANCH"
fi
How CodePulse uses it: CodePulse can inject project-specific instructions from your Telegram configuration into every prompt.
Tool Events
These are the most commonly used hooks. They fire inside the agentic loop, potentially many times per session.
PreToolUse
When it fires: Before a tool call executes. Can block it.
Matcher values: Tool names -- Bash, Edit, Write, Read, Glob, Grep, Agent, WebFetch, WebSearch, and MCP tool names like mcp__github__create_issue.
Use cases: Blocking dangerous commands, modifying tool input, enforcing policies.
Decision control: Uses hookSpecificOutput with permissionDecision:
"allow"-- bypass the permission system entirely"deny"-- block the tool call, reason shown to Claude"ask"-- escalate to the user for manual confirmation
#!/bin/bash
# .claude/hooks/block-force-push.sh
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if [ "$TOOL" = "Bash" ] && echo "$COMMAND" | grep -q 'git push.*--force'; then
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Force push is blocked by policy"}}'
else
exit 0
fi
How CodePulse uses it: This is CodePulse's primary hook. Every permission request flows through PreToolUse. CodePulse evaluates the request against its auto-approval patterns and either auto-approves, auto-denies, or routes it to Telegram for your decision.
PermissionRequest
When it fires: When a permission dialog is about to be shown to the user.
Matcher values: Same tool names as PreToolUse.
Use cases: Auto-approving known-safe operations, denying operations programmatically.
Decision control: Uses hookSpecificOutput with a decision object containing behavior: "allow" or behavior: "deny". Can also modify tool input with updatedInput and persist permission rules with updatedPermissions.
#!/bin/bash
# .claude/hooks/auto-approve-tests.sh
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if echo "$COMMAND" | grep -qE '^(npm test|npx jest|npx vitest)'; then
echo '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}'
fi
How CodePulse uses it: CodePulse intercepts permission dialogs and forwards them to Telegram as interactive cards with Allow/Deny buttons. When you respond, CodePulse returns the decision through the hook's output.
PostToolUse
When it fires: After a tool call completes successfully.
Matcher values: Same tool names as PreToolUse.
Use cases: Running linters after file writes, logging operations, triggering side effects.
Decision control: Return {"decision": "block", "reason": "..."} to provide feedback to Claude (the tool already ran, so this does not undo it).
#!/bin/bash
# .claude/hooks/lint-on-write.sh
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
if [[ "$FILE" == *.ts ]] || [[ "$FILE" == *.tsx ]]; then
RESULT=$(npx eslint "$FILE" 2>&1)
if [ $? -ne 0 ]; then
echo "{\"decision\": \"block\", \"reason\": \"ESLint errors in $FILE: $RESULT\"}"
fi
fi
How CodePulse uses it: CodePulse uses PostToolUse to track The Pulse activity feed. Every successful tool call is logged and, if significant, forwarded to your Telegram as a real-time update.
PostToolUseFailure
When it fires: After a tool call fails.
Matcher values: Same tool names as PreToolUse.
Use cases: Error logging, sending alerts, providing corrective context to Claude.
Note: Cannot block or undo the failure. The error field contains what went wrong, and is_interrupt indicates if the user cancelled it.
How CodePulse uses it: CodePulse sends failure notifications to Telegram so you know when Claude's tool calls are failing repeatedly.
Notification Events
Notification
When it fires: When Claude Code sends a notification.
Matcher values: permission_prompt, idle_prompt, auth_success, elicitation_dialog
Use cases: Custom notification routing, alerting on specific event types.
{
"hooks": {
"Notification": [
{
"matcher": "permission_prompt",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/notify-telegram.sh"
}
]
}
]
}
}
How CodePulse uses it: CodePulse intercepts all notification types and routes them to Telegram. Permission prompts become interactive cards. Idle prompts let you know Claude is waiting for input.
Subagent Events
SubagentStart
When it fires: When a subagent is spawned via the Agent tool.
Matcher values: Agent type names -- Bash, Explore, Plan, or custom agent names.
Use cases: Logging subagent spawns, injecting context into subagents.
How CodePulse uses it: CodePulse logs subagent creation for The Pulse activity feed.
SubagentStop
When it fires: When a subagent finishes responding.
Matcher values: Same as SubagentStart.
Decision control: Same as Stop hooks. Return {"decision": "block", "reason": "..."} to prevent the subagent from stopping.
Agent Response Events
Stop
When it fires: When the main Claude Code agent finishes responding. Does not fire on user interrupts.
Matcher: No matcher support. Fires on every stop.
Decision control: Return {"decision": "block", "reason": "..."} to prevent Claude from stopping. Claude will continue working based on the reason you provide.
Important: The stop_hook_active field is true when Claude is already continuing due to a previous Stop hook. Check this field to avoid infinite loops.
#!/bin/bash
# .claude/hooks/verify-tests.sh
INPUT=$(cat)
ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active')
# Avoid infinite loops
if [ "$ACTIVE" = "true" ]; then
exit 0
fi
# Run tests before allowing Claude to stop
if ! npm test > /dev/null 2>&1; then
echo '{"decision":"block","reason":"Tests are failing. Fix them before finishing."}'
fi
How CodePulse uses it: CodePulse sends a session completion notification to Telegram when Claude stops, including a summary of what was accomplished.
Team Events
TeammateIdle
When it fires: When an agent team teammate is about to go idle.
Matcher: No matcher support.
Decision control: Exit code 2 with stderr feedback prevents the teammate from going idle.
TaskCompleted
When it fires: When a task is being marked as completed.
Matcher: No matcher support.
Decision control: Exit code 2 with stderr feedback prevents the task from being marked complete.
Configuration Events
ConfigChange
When it fires: When a configuration file changes during a session.
Matcher values: user_settings, project_settings, local_settings, policy_settings, skills
Decision control: Return {"decision": "block", "reason": "..."} to prevent the change. Note: policy_settings changes cannot be blocked.
Worktree Events
WorktreeCreate
When it fires: When a worktree is being created via --worktree or isolation: "worktree".
Special behavior: Replaces the default git worktree behavior. The hook must print the absolute path to the created worktree on stdout.
Use cases: Using non-git version control systems (SVN, Perforce, Mercurial).
WorktreeRemove
When it fires: When a worktree is being removed.
Decision control: Cannot block removal. Used for cleanup.
Compaction Events
PreCompact
When it fires: Before context compaction occurs.
Matcher values: manual (from /compact command), auto (when context window is full)
Use cases: Saving important context before it gets compacted, injecting compaction instructions.
Quick Reference Table
| Event | Matcher | Can Block? | Hook Types |
|---|---|---|---|
| SessionStart | startup, resume, clear, compact | No | command |
| UserPromptSubmit | none | Yes | command, http, prompt, agent |
| PreToolUse | tool name | Yes | command, http, prompt, agent |
| PermissionRequest | tool name | Yes | command, http, prompt, agent |
| PostToolUse | tool name | No | command, http, prompt, agent |
| PostToolUseFailure | tool name | No | command, http, prompt, agent |
| Notification | notification type | No | command |
| SubagentStart | agent type | No | command |
| SubagentStop | agent type | Yes | command, http, prompt, agent |
| Stop | none | Yes | command, http, prompt, agent |
| TeammateIdle | none | Yes | command |
| TaskCompleted | none | Yes | command, http, prompt, agent |
| ConfigChange | config source | Yes | command |
| WorktreeCreate | none | Yes | command |
| WorktreeRemove | none | No | command |
| PreCompact | manual, auto | No | command |
| SessionEnd | exit reason | No | command |
Advanced: Prompt and Agent Hooks
Beyond shell commands and HTTP endpoints, Claude Code supports two LLM-powered hook types.
Prompt hooks (type: "prompt") send a single prompt to a fast Claude model and expect a {"ok": true/false, "reason": "..."} response. Useful for natural-language policy checks.
Agent hooks (type: "agent") spawn a subagent that can use Read, Grep, and Glob tools to investigate before making a decision. Useful when verification requires inspecting actual files.
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Check if all tasks in the conversation are complete: $ARGUMENTS",
"timeout": 30
}
]
}
]
}
}
How CodePulse Uses the Hook System
CodePulse registers handlers for the hook events that matter most for remote monitoring and control:
- PreToolUse and PermissionRequest power the Approval Pipeline, routing every permission decision through Telegram
- PostToolUse feeds The Pulse real-time activity tracker
- Notification forwards Claude Code alerts to Telegram
- Stop triggers session completion summaries
- SessionStart and SessionEnd manage session lifecycle and the Morning Briefing
Instead of writing and maintaining these hooks yourself, CodePulse installs them automatically with a single command (npm run install:hooks) and handles all the JSON parsing, Telegram routing, and decision logic.
Install CodePulse and get all of this working in about two minutes. The free tier includes the full hook integration.