Hooks System
Hooks let you automatically run shell commands, LLM prompts, or agents at specific points in the Claude Code lifecycle. You can build repetitive tasks โ running tests, formatting, notifications, logging, security checks โ directly into your coding workflow.
What Are Hooks?โ
When Claude Code edits a file or runs a command, hooks let you "step in" to that action and perform additional work.
Claude receives a file edit request
โ
[PreToolUse hook runs] โ runs before the edit (can block)
โ
Claude edits the file
โ
[PostToolUse hook runs] โ runs after the edit (provides feedback)
โ
Next task proceeds
The 18 Hook Eventsโ
| Hook event | When it runs | Can block |
|---|---|---|
SessionStart | When a session starts or resumes | - |
UserPromptSubmit | When the user submits a prompt (before processing) | O |
InstructionsLoaded | When CLAUDE.md or .claude/rules/*.md is loaded | - |
PreToolUse | Right before a tool runs | O |
PermissionRequest | When a permission dialog is shown | O |
PostToolUse | After a tool runs successfully | - |
PostToolUseFailure | After a tool run fails | - |
Notification | When a notification is sent | - |
SubagentStart | When a subagent is created | - |
SubagentStop | When a subagent finishes | O |
Stop | When Claude finishes responding | O |
TeammateIdle | When an Agent Teams teammate goes idle | O |
TaskCompleted | When a task is marked complete | O |
ConfigChange | When a settings file changes | O |
WorktreeCreate | When a worktree is created | O |
WorktreeRemove | When a worktree is removed | - |
PreCompact | Right before context compaction | - |
SessionEnd | When a session ends | - |
The 4 Hook Typesโ
Command Hook (shell command)โ
The most basic type. Runs a shell command and receives JSON input on stdin.
{
"type": "command",
"command": "npx prettier --write \"$CLAUDE_PROJECT_DIR/src\""
}
Prompt Hook (LLM evaluation)โ
Sends a prompt to a Claude model and gets a yes/no decision. This enables intelligent validation without writing any code.
{
"type": "prompt",
"prompt": "Evaluate whether this change completed all the tasks: $ARGUMENTS",
"timeout": 30
}
The LLM responds in the form {"ok": true} or {"ok": false, "reason": "explanation"}.
Agent Hook (agent verification)โ
Spawns a subagent that performs multi-step verification using tools like file reads and code search.
{
"type": "agent",
"prompt": "Run the test suite and confirm that all unit tests pass. $ARGUMENTS",
"timeout": 120
}
HTTP Hook (external endpoint)โ
Sends a POST request directly to an external HTTP endpoint. It connects to Slack webhooks, monitoring services, logging systems, and the like without needing a shell script.
{
"type": "http",
"url": "https://hooks.slack.com/services/T00/B00/xxxxx",
"timeout": 30
}
The hook event's JSON input is sent as the HTTP POST body.
Prompt/Agent hooks are only available for PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, UserPromptSubmit, Stop, SubagentStop, and TaskCompleted. The remaining events support Command/HTTP hooks only.
Configurationโ
Hooks are configured in settings.json using a three-level nested structure:
- Choose a hook event (e.g.
PostToolUse) - Filter with a matcher pattern (e.g. "only the Write|Edit tools")
- Define the hook handler (the command/prompt to run)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "npx prettier --write \"$CLAUDE_PROJECT_DIR/src\""
}
]
}
]
}
}
Where Hooks Can Be Configuredโ
| Location | Scope | Sharing |
|---|---|---|
~/.claude/settings.json | All projects | Local machine only |
.claude/settings.json | Single project | Can be committed to git |
.claude/settings.local.json | Single project | gitignored |
| Managed policy | Organization-wide | Admin-controlled |
Plugin hooks/hooks.json | While the plugin is active | Bundled with the plugin |
| Skills/Agent frontmatter | While the component is active | Inside the component file |
The /hooks Interactive Menuโ
/hooks
You can view, add, and delete hooks without editing settings files directly. Each hook shows its source: [User], [Project], [Local], [Plugin].
Matcher Patternsโ
A matcher is a regular expression that determines when the hook runs. Using "*", "", or omitting it entirely runs the hook in all cases. Identifiers containing hyphens (e.g. code-reviewer) must match exactly rather than partially (v2.1.195+).
What Each Event Matches Againstโ
| Event | Matches against | Examples |
|---|---|---|
PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest | Tool name | Bash, Edit|Write, mcp__.* |
SessionStart | How the session started | startup, resume, clear, compact |
SessionEnd | Exit reason | clear, logout, prompt_input_exit, bypass_permissions_disabled, other |
Notification | Notification type | permission_prompt, idle_prompt, auth_success, elicitation_dialog, agent_needs_input / agent_completed (claude agents background sessions, v2.1.198+) |
SubagentStart, SubagentStop | Agent type | Bash, Explore, Plan, custom agent names |
ConfigChange | Settings source | user_settings, project_settings, local_settings, policy_settings, skills |
PreCompact | Trigger method | manual, auto |
UserPromptSubmit, Stop, TeammateIdle, TaskCompleted, WorktreeCreate, WorktreeRemove, InstructionsLoaded | Matching not supported | Always runs |
Matching MCP Toolsโ
MCP tools follow the mcp__<server>__<tool> format and are matched just like regular tools:
"matcher": "mcp__memory__.*" // all tools from the memory server
"matcher": "mcp__.*__write.*" // write-related tools from any server
"matcher": "mcp__brave-search__.*" // all tools from a hyphenated server
Server names containing hyphens are not partially matched, so to catch every tool from such a server you must spell out the .*, as in mcp__brave-search__.* (v2.1.195+).
JSON Input and Outputโ
Hooks receive JSON on stdin and return JSON on stdout.
Common Input Fieldsโ
Base fields that every event receives:
| Field | Description |
|---|---|
session_id | Session ID |
transcript_path | Path to the conversation transcript |
cwd | Current working directory |
permission_mode | Current permission mode |
hook_event_name | Name of the event that fired |
PreToolUse example:
{
"session_id": "abc123",
"cwd": "/home/user/my-project",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": { "command": "npm test" }
}
Exit Codesโ
| Exit code | Meaning |
|---|---|
0 | Success โ the JSON on stdout is processed |
2 | Block โ the stderr message is passed to Claude |
| Other | Non-blocking error โ execution continues |
JSON Output Fieldsโ
{
"continue": false,
"stopReason": "Build failed. Fix the errors before continuing",
"suppressOutput": false,
"systemMessage": "Warning to show the user"
}
| Field | Description |
|---|---|
continue | If false, stops Claude's execution entirely |
stopReason | Shown to the user when continue: false |
suppressOutput | If true, hides verbose output |
systemMessage | Warning displayed to the user |
Key Events in Detailโ
SessionStartโ
Runs when a session starts. Useful for loading development context and setting environment variables.
CLAUDE_ENV_FILE โ Persisting Environment Variablesโ
The CLAUDE_ENV_FILE environment variable, available only in SessionStart hooks, lets environment variables set by the hook be used by every Bash command in the session:
#!/bin/bash
if [ -n "$CLAUDE_ENV_FILE" ]; then
echo 'export NODE_ENV=production' >> "$CLAUDE_ENV_FILE"
echo 'export PATH="$PATH:./node_modules/.bin"' >> "$CLAUDE_ENV_FILE"
fi
exit 0
Injecting Contextโ
You can provide additional context to Claude by printing to stdout or via the additionalContext field in JSON:
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "Current issue: #42, last deploy: 2 hours ago"
}
}
PreToolUseโ
Runs before a tool executes. It can decide allow, deny, or ask:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "rm -rf commands are blocked by a hook"
}
}
| Decision | Meaning |
|---|---|
allow | Bypasses the permission system and runs immediately |
deny | Blocks the tool call and passes the reason to Claude |
ask | Shows a confirmation prompt to the user |
You can also modify the tool input with updatedInput:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": { "command": "npm run lint -- --fix" }
}
}
PostToolUseโ
Runs after a tool executes successfully. It can provide feedback about the tool result to Claude.
{
"decision": "block",
"reason": "Lint errors found. Fix them before continuing"
}
Stopโ
Runs when Claude finishes responding. With decision: "block" you can keep Claude working instead of stopping.
{
"decision": "block",
"reason": "Tests are still failing. Make all tests pass before finishing"
}
In Stop hooks, check the stop_hook_active field. It is true when Claude is already continuing because of a Stop hook. Ignoring it can make Claude run forever.
SubagentStart / SubagentStopโ
Detects subagent creation and completion. You can filter by agent type with a matcher:
{
"hooks": {
"SubagentStart": [
{
"matcher": "Explore",
"hooks": [{
"type": "command",
"command": "echo 'Explore agent started' >> ~/agent-log.txt"
}]
}
]
}
}
TeammateIdle / TaskCompletedโ
Run when an Agent Teams teammate goes idle or a task is marked complete. Useful as quality gates:
#!/bin/bash
# TaskCompleted hook โ only allow task completion when tests pass
if ! npm test 2>&1; then
echo "Tests failed. Fix the tests before completing." >&2
exit 2
fi
exit 0
WorktreeCreate / WorktreeRemoveโ
Run when using --worktree or isolation: "worktree". If you use a VCS other than git (SVN, Perforce, etc.), you can replace the default git worktree behavior.
ConfigChangeโ
Runs when a settings file changes during a session. Useful for security audit logging or blocking unauthorized configuration changes.
Async Hooksโ
Set "async": true to run a hook in the background without blocking Claude:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/run-tests-async.sh",
"async": true,
"timeout": 300
}
]
}
]
}
}
Async hook results are delivered on the next conversation turn. They cannot make decisions (block/allow). Only Command hooks are supported.
Hooks in Skills and Agentsโ
You can define hooks directly in the YAML frontmatter of Skills and subagents. They run only while that component is active:
---
name: secure-operations
description: Perform operations with security validation
hooks:
PreToolUse:
- matcher: "Bash"
hooks:
- type: command
command: "./scripts/security-check.sh"
---
Environment Variable Referenceโ
| Variable | Description |
|---|---|
$CLAUDE_PROJECT_DIR | Project root path |
${CLAUDE_PLUGIN_ROOT} | Plugin root directory |
$CLAUDE_ENV_FILE | Environment variable persistence file (SessionStart only) |
$CLAUDE_CODE_REMOTE | "true" in remote web environments |
When bundling hook scripts inside your project, use $CLAUDE_PROJECT_DIR so the path resolves correctly regardless of the working directory:
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-style.sh"
Practical Examplesโ
Auto-Format on File Saveโ
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "npx prettier --write \"$(cat | jq -r '.tool_input.file_path')\" 2>/dev/null"
}
]
}
]
}
}
Blocking Dangerous Commandsโ
#!/bin/bash
# .claude/hooks/block-rm.sh
COMMAND=$(jq -r '.tool_input.command')
if echo "$COMMAND" | grep -q 'rm -rf'; then
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "rm -rf commands are blocked by a hook"
}
}'
else
exit 0
fi
Desktop Notification on Completion (macOS)โ
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude task complete\" with title \"Claude Code\"'"
}
]
}
]
}
}
Desktop Notification on Completion (Windows)โ
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "powershell -Command \"[System.Windows.Forms.MessageBox]::Show('Claude task complete')\""
}
]
}
]
}
}
Validating Stop with a Prompt Hookโ
Use an LLM to evaluate task completion without writing any code:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Analyze the conversation and determine: 1) whether all requested tasks are complete 2) whether any errors remain unresolved 3) whether follow-up work is needed. $ARGUMENTS",
"timeout": 30
}
]
}
]
}
}
Recommended Hook Setupโ
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "npx prettier --write \"$(cat | jq -r '.tool_input.file_path')\" 2>/dev/null; npx eslint \"$(cat | jq -r '.tool_input.file_path')\" --fix --quiet 2>&1 | head -10"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "echo \"[$(date '+%H:%M:%S')] Task complete\" >> ~/.claude-sessions.log"
}
]
}
]
}
}
Caveatsโ
Hooks run with the full permissions of your system user.
- Always quote shell variables (
"$VAR") - Block
..(path traversal) in file paths - Avoid sensitive files like
.env,.git/, and key files - Do not apply
.claude/settings.jsonfrom untrusted projects as-is
Slow hook commands make Claude Code feel sluggish. Run long tasks in the background with "async": true.
Run claude --debug to see detailed hook execution logs. Toggle verbose mode with Ctrl+O to watch hook progress in the transcript.