Skip to main content

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 eventWhen it runsCan block
SessionStartWhen a session starts or resumes-
UserPromptSubmitWhen the user submits a prompt (before processing)O
InstructionsLoadedWhen CLAUDE.md or .claude/rules/*.md is loaded-
PreToolUseRight before a tool runsO
PermissionRequestWhen a permission dialog is shownO
PostToolUseAfter a tool runs successfully-
PostToolUseFailureAfter a tool run fails-
NotificationWhen a notification is sent-
SubagentStartWhen a subagent is created-
SubagentStopWhen a subagent finishesO
StopWhen Claude finishes respondingO
TeammateIdleWhen an Agent Teams teammate goes idleO
TaskCompletedWhen a task is marked completeO
ConfigChangeWhen a settings file changesO
WorktreeCreateWhen a worktree is createdO
WorktreeRemoveWhen a worktree is removed-
PreCompactRight before context compaction-
SessionEndWhen 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.

Supported events

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:

  1. Choose a hook event (e.g. PostToolUse)
  2. Filter with a matcher pattern (e.g. "only the Write|Edit tools")
  3. 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โ€‹

LocationScopeSharing
~/.claude/settings.jsonAll projectsLocal machine only
.claude/settings.jsonSingle projectCan be committed to git
.claude/settings.local.jsonSingle projectgitignored
Managed policyOrganization-wideAdmin-controlled
Plugin hooks/hooks.jsonWhile the plugin is activeBundled with the plugin
Skills/Agent frontmatterWhile the component is activeInside 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โ€‹

EventMatches againstExamples
PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequestTool nameBash, Edit|Write, mcp__.*
SessionStartHow the session startedstartup, resume, clear, compact
SessionEndExit reasonclear, logout, prompt_input_exit, bypass_permissions_disabled, other
NotificationNotification typepermission_prompt, idle_prompt, auth_success, elicitation_dialog, agent_needs_input / agent_completed (claude agents background sessions, v2.1.198+)
SubagentStart, SubagentStopAgent typeBash, Explore, Plan, custom agent names
ConfigChangeSettings sourceuser_settings, project_settings, local_settings, policy_settings, skills
PreCompactTrigger methodmanual, auto
UserPromptSubmit, Stop, TeammateIdle, TaskCompleted, WorktreeCreate, WorktreeRemove, InstructionsLoadedMatching not supportedAlways 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:

FieldDescription
session_idSession ID
transcript_pathPath to the conversation transcript
cwdCurrent working directory
permission_modeCurrent permission mode
hook_event_nameName 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 codeMeaning
0Success โ€” the JSON on stdout is processed
2Block โ€” the stderr message is passed to Claude
OtherNon-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"
}
FieldDescription
continueIf false, stops Claude's execution entirely
stopReasonShown to the user when continue: false
suppressOutputIf true, hides verbose output
systemMessageWarning 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"
}
}
DecisionMeaning
allowBypasses the permission system and runs immediately
denyBlocks the tool call and passes the reason to Claude
askShows 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"
}
Preventing infinite loops

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โ€‹

VariableDescription
$CLAUDE_PROJECT_DIRProject root path
${CLAUDE_PLUGIN_ROOT}Plugin root directory
$CLAUDE_ENV_FILEEnvironment variable persistence file (SessionStart only)
$CLAUDE_CODE_REMOTE"true" in remote web environments
Referencing project scripts

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
}
]
}
]
}
}
{
"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โ€‹

Hook security

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.json from untrusted projects as-is
Hook performance

Slow hook commands make Claude Code feel sluggish. Run long tasks in the background with "async": true.

Debugging

Run claude --debug to see detailed hook execution logs. Toggle verbose mode with Ctrl+O to watch hook progress in the transcript.