Claude Code hooks generator

Pick a hook event, a tool matcher, and a command — or start from a recipe like auto-format on save — and copy settings.json that Claude Code accepts.

Start from a recipe
Hook 1 · can't block

Fires when right after a tool call succeeds. Exit 2: Shows stderr to Claude — the tool already ran.

  • TipUses jq to read the JSON on stdin — install it (brew install jq / apt-get install jq).
Settings file
Save as

.claude/settings.json

{
  "$schema": "https://json.schemastore.org/claude-code-settings.json",
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ]
  }
}
Write it from the terminal (overwrites the file — paste your current one above first)
mkdir -p .claude && cat > .claude/settings.json <<'EOF'
{
  "$schema": "https://json.schemastore.org/claude-code-settings.json",
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ]
  }
}
EOF

Claude Code's file watcher normally picks up hook edits without a restart. Run /hooks in a session to confirm the hook is registered and see which file it came from.

Test your hook — the JSON Claude Code sends on stdin
Sample PostToolUse input (tool_input depends on the tool)
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project",
  "permission_mode": "default",
  "hook_event_name": "PostToolUse",
  "tool_name": "Write",
  "tool_input": {
    "file_path": "/Users/you/my-project/src/index.ts",
    "content": "export const hello = 'world'\n"
  },
  "tool_response": {
    "filePath": "/Users/you/my-project/src/index.ts",
    "type": "create"
  },
  "tool_use_id": "toolu_01ABC123",
  "duration_ms": 12
}
Pipe it into the hook and print the exit code
printf '%s\n' '{"session_id":"abc123","transcript_path":"/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl","cwd":"/Users/you/my-project","permission_mode":"default","hook_event_name":"PostToolUse","tool_name":"Write","tool_input":{"file_path":"/Users/you/my-project/src/index.ts","content":"export const hello = '\''world'\''\n"},"tool_response":{"filePath":"/Users/you/my-project/src/index.ts","type":"create"},"tool_use_id":"toolu_01ABC123","duration_ms":12}' | CLAUDE_PROJECT_DIR="$PWD" sh -c 'jq -r '\''.tool_input.file_path'\'' | xargs npx prettier --write'; echo "exit code: $?"

Exit 0 = no objection (stdout goes to the debug log). Exit 2 on PostToolUse: Shows stderr to Claude — the tool already ran. Any other code is a non-blocking error. The command really runs — edit the sample paths before testing something that writes files.

What are hooks in Claude Code?

Hooks are user-defined handlers — usually shell commands — that Claude Code runs automatically at fixed points in its lifecycle. Where a CLAUDE.md instruction asks the model to remember something, a hook makes it happen every time: format after every edit, refuse writes to .env, run tests before Claude says it's done, ping your desktop when it needs you.

The configuration has three levels: a hook event (when), a matcher group (which occurrences), and one or more hook handlers (what runs). All of it lives under one hooks object in a settings file — add new events as siblings, don't replace the object:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write" }
        ]
      }
    ],
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          { "type": "command", "command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'" }
        ]
      }
    ]
  }
}

When an event fires and the matcher matches, Claude Code sends JSON describing the event to your handler — on stdin for command hooks — and reads back an exit code and optional JSON. Matching handlers run in parallel, and an identical handler defined in two settings files runs once.

Claude Code hooks events list (2026)

All 33 events in the current hooks reference. Per session: SessionStart and SessionEnd. Per turn: UserPromptSubmit, Stop, StopFailure. On every tool call: PreToolUse and PostToolUse. The rest fire around subagents, compaction, model switches, files, and MCP servers. Click an event name for its stdin fields and a sample payload.

EventFires whenMatcher filtersCan block
SessionStarta session starts, resumes, or restarts after /clear or compactionHow the session startedNo
Setuponly with claude --init-only, or -p with --init or --maintenance — never on a normal startWhich flag triggered itNo
InstructionsLoadeda CLAUDE.md or .claude/rules/*.md file loads into context — at start and when loaded lazilyLoad reasonNo
UserPromptSubmityou submit a prompt, before Claude processes itNo matcherYes
UserPromptExpansiona slash command or MCP prompt you typed expands into a prompt, before Claude sees itCommand nameYes
MessageDisplayassistant text streams to the screen, once per batch of completed linesNo matcherNo
PreToolUseClaude has built a tool call, before it runsTool nameYes
PermissionRequestClaude Code is about to show you a permission promptTool nameNo
PermissionDeniedauto mode denies a tool call (not manual denials, deny rules, or hook blocks)Tool nameNo
PostToolUseright after a tool call succeedsTool nameNo
PostToolUseFailurea tool that started running fails (not validation errors or permission denials)Tool nameNo
PostToolBatchevery tool call in a parallel batch has resolved, before the next model callNo matcherYes
NotificationClaude Code sends a notification — e.g. it needs permission or has been idleNotification typeNo
SubagentStarta subagent is spawned or resumedAgent typeNo
SubagentStopa subagent finishes respondingAgent typeYes
TaskCreateda task is being created with the TaskCreate toolNo matcherYes
TaskCompleteda task is being marked completedNo matcherYes
Stopthe main agent finishes responding (not on your interrupt; API errors fire StopFailure)No matcherYes
StopFailurethe turn ends because of an API errorError typeNo
TeammateIdlean agent team teammate is about to go idleNo matcherYes
ConfigChangea settings, managed policy, or skill file changes during a sessionConfiguration sourceYes
CwdChangedthe working directory changes, e.g. Claude runs cdNo matcherNo
DirectoryAddeda working directory is added mid-session with /add-dir (or the SDK)How it was addedNo
FileChangeda watched file changes on disk, whatever changed itLiteral filenames to watch, split on |No
WorktreeCreatea worktree is created (--worktree, isolation: worktree, background sessions)No matcherYes
WorktreeRemovea worktree is removed at session exit, subagent finish, or background-session deleteNo matcherYes
PreCompactright before context compactionWhat triggered compactionYes
PostCompactafter compaction completesWhat triggered compactionNo
PreModelSwitchbefore a model switch you or a client requested (/model, the picker, /config)Canonical name of the target modelYes
PostModelSwitchafter the session's model changes, including automatic fallbacks and resumeCanonical name of the new modelNo
SessionEnda session ends: exit, /clear, logout, or switching with /resumeWhy it endedNo
Elicitationan MCP server asks for user input during a tool callMCP server nameYes
ElicitationResultyou answered an MCP elicitation, before the answer goes back to the serverMCP server nameYes

Claude Code hooks reference: input, exit codes, output

Common input fields

Every event's JSON includes these, plus the event-specific fields listed further down:

FieldMeaning
session_idCurrent session ID
transcript_pathPath to the conversation JSONL — may lag the current turn; Stop hooks should read last_assistant_message instead
cwdWorking directory when the hook runs (follows Claude into worktrees and cd)
hook_event_nameThe event that fired
permission_modeDefault, plan, acceptEdits, auto, dontAsk, or bypassPermissions — not on every event
prompt_idUUID of the prompt being processed (absent before the first input)
agent_id, agent_typePresent when the hook fires inside a subagent or with --agent
effort{ level } on events inside a tool-use context, when the model supports effort

Exit codes

Exit codeWhat happens
0Success. stdout goes to the debug log — except on UserPromptSubmit, UserPromptExpansion, SessionStart, and PostModelSwitch, where plain-text stdout is added to Claude's context. stdout that starts with { and ends with } is parsed as JSON output
2Blocking error on events that can block: the action is stopped and stderr becomes the reason (a JSON blocking reason wins if you print one). Even a JSON "allow" can't override it. On events that can't block, see each event's row
anything elseNon-blocking error: the action proceeds and the transcript shows a hook error notice with the first line of stderr. Exit 1 does not block — use 2 for policy hooks

A mistyped script path exits 127 — a non-blocking error — so a broken policy hook silently lets everything through. Watch for the hook error notice on its first run.

JSON output fields any hook can return

FieldEffect
continuefalse stops Claude entirely after the hook (takes precedence over decisions)
stopReasonMessage shown when continue is false
systemMessageWarning shown to you (some events discard it)
terminalSequenceAn allowlisted escape sequence Claude Code emits for you — desktop notifications (OSC 9 / 99 / 777), window titles, bell
hookSpecificOutputEvent-specific control; must include hookEventName
decision / reasonTop-level "block" for UserPromptSubmit, PostToolUse, Stop, SubagentStop, PreCompact, ConfigChange and similar

additionalContext, systemMessage, and plain stdout are capped at 10,000 characters each; longer output is saved to a file and Claude gets the path plus a preview. Your stdout must contain only the JSON — a shell profile that echoes on startup breaks parsing.

Decision control by event

EventsHow to decide
PreToolUsehookSpecificOutput.permissionDecision allow / deny / ask / defer + permissionDecisionReason; updatedInput rewrites the arguments. Across hooks, deny > defer > ask > allow
PermissionRequesthookSpecificOutput.decision.behavior allow / deny, with updatedInput, updatedPermissions, message, interrupt
UserPromptSubmit, UserPromptExpansion, PostToolUse, PostToolUseFailure, PostToolBatch, Stop, SubagentStop, ConfigChange, PreCompactTop-level "decision": "block" + reason, and additionalContext where supported
PreModelSwitchpermissionDecision allow / deny / ask, or decision block
TeammateIdle, TaskCompleted, TaskCreatedExit 2, or continue false (TaskCreated: decision block)
SessionStart, SubagentStart, PostModelSwitchContext only — additionalContext
WorktreeCreatePrint the new path on stdout
Elicitation, ElicitationResultAction accept / decline / cancel + content
Setup, Notification, SessionEnd, PostCompact, InstructionsLoaded, StopFailure, CwdChanged, DirectoryAdded, FileChangedNone — side effects only

A PreToolUse allow skips the prompt but never overrides a deny or ask rule from your settings — hooks can tighten permissions, not loosen them. A PreToolUse deny, on the other hand, blocks even in bypassPermissions mode.

Hook events: stdin fields and sample payloads

Session lifecycle

SessionStart— a session starts, resumes, or restarts after /clear or compaction

Matcher: How the session started — startup, resume, clear, compact, fork

Exit 2: Shows stderr to you only; the session continues.

JSON output: Plain stdout is added to Claude's context. JSON: additionalContext, sessionTitle, initialUserMessage (-p only), watchPaths, reloadSkills. Has access to $CLAUDE_ENV_FILE to persist env vars.

Handler types: command, mcp_tool

Note: Runs on every session, so keep it fast. Only command and mcp_tool hooks run here, and mcp_tool hooks are skipped at launch.

  • source — startup, resume, clear, compact, or fork
  • model — Active model ID — sometimes omitted, check before reading
  • agent_type — Present with claude --agent <name>
  • session_title — Current title, if one is set
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project",
  "hook_event_name": "SessionStart",
  "source": "startup",
  "model": "claude-opus-5"
}
Setup— only with claude --init-only, or -p with --init or --maintenance — never on a normal start

Matcher: Which flag triggered it — init, maintenance

Exit 2: Exit code and stderr are ignored.

JSON output: None — JSON output is discarded. Has access to $CLAUDE_ENV_FILE.

Handler types: command, mcp_tool

  • trigger — init or maintenance
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project",
  "hook_event_name": "Setup",
  "trigger": "init"
}
Notification— Claude Code sends a notification — e.g. it needs permission or has been idle

Matcher: Notification type — permission_prompt, idle_prompt, auth_success, elicitation_dialog, elicitation_url_dialog, elicitation_complete, elicitation_response, agent_needs_input, agent_completed, quota_auto_resume_fired, quota_auto_resume_stale, quota_auto_resume_disabled

Exit 2: Exit code and stderr are ignored.

JSON output: None — side effects only. terminalSequence still fires (desktop notifications via your terminal).

Handler types: command, http, mcp_tool

Note: permission_prompt fires after the prompt waits ~6 seconds; idle_prompt ~60 seconds after Claude finishes. For an instant signal on permission prompts, use PermissionRequest.

  • message — Notification text
  • title — Optional title
  • notification_type — Which type fired (the matcher field)
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project",
  "hook_event_name": "Notification",
  "message": "Claude needs your permission",
  "title": "Permission needed",
  "notification_type": "permission_prompt"
}
SessionEnd— a session ends: exit, /clear, logout, or switching with /resume

Matcher: Why it ended — clear, resume, logout, prompt_input_exit, other

Exit 2: Shows stderr to you only.

JSON output: None — cleanup and logging; JSON output is discarded

Handler types: command, http, mcp_tool

Note: All SessionEnd hooks share a 1.5-second budget. A longer per-hook timeout raises it, up to 60 seconds.

  • reason — Why the session ended (the matcher field)
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project",
  "hook_event_name": "SessionEnd",
  "reason": "other"
}

Prompts and display

UserPromptSubmit— you submit a prompt, before Claude processes it

Matcher: Not supported; fires on every occurrence

Exit 2: Blocks the prompt and erases it; stderr is shown to you, not Claude.

JSON output: Plain stdout is added as context. JSON: decision "block" + reason; hookSpecificOutput.additionalContext, sessionTitle, suppressOriginalPrompt. It can't rewrite the prompt.

Handler types: command, http, mcp_tool, prompt, agent

Note: Default timeout is 30 seconds here (not 600), because it runs before every prompt.

  • prompt — The text you submitted, pasted content expanded
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project",
  "permission_mode": "default",
  "hook_event_name": "UserPromptSubmit",
  "prompt": "Write a function to calculate the factorial of a number"
}
UserPromptExpansion— a slash command or MCP prompt you typed expands into a prompt, before Claude sees it

Matcher: Command name — your skill or command names

Exit 2: Blocks the expansion; stderr is shown to you.

JSON output: JSON: decision "block" + reason; hookSpecificOutput.additionalContext

Handler types: command, http, mcp_tool, prompt, agent

Note: Typing /skill directly bypasses PreToolUse on the Skill tool — this event covers that path.

  • expansion_type — slash_command or mcp_prompt
  • command_name — The command (the matcher field)
  • command_args — Text after the command
  • command_source — Where the command came from, e.g. plugin
  • prompt — The original typed text
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project",
  "permission_mode": "default",
  "hook_event_name": "UserPromptExpansion",
  "expansion_type": "slash_command",
  "command_name": "deploy",
  "command_args": "staging",
  "command_source": "project",
  "prompt": "/deploy staging"
}
MessageDisplay— assistant text streams to the screen, once per batch of completed lines

Matcher: Not supported; fires on every occurrence

Exit 2: The original text is displayed.

JSON output: JSON: hookSpecificOutput.displayContent replaces what's shown on screen only — Claude and the transcript keep the original

Handler types: command, http, mcp_tool

Note: Default timeout is 10 seconds; each batch waits for your hook, so keep it fast.

  • turn_id — UUID of the turn
  • message_id — UUID of the message, stable across batches
  • index — 0-based batch index within the message
  • final — True on the last batch
  • delta — The newly completed lines
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project",
  "hook_event_name": "MessageDisplay",
  "turn_id": "0c9e6a2f-7d41-4f4e-9a15-3f4f7c2b8d10",
  "message_id": "5b2a9c8e-1f63-4d8a-b7c4-9e0d2a6f1c3b",
  "index": 0,
  "final": false,
  "delta": "Here is the plan:\n"
}

Tool calls

PreToolUse— Claude has built a tool call, before it runs

Matcher: Tool name — Bash, Edit|Write, Read, mcp__.*, *

Exit 2: Blocks the tool call; stderr goes to Claude as the reason.

JSON output: JSON: hookSpecificOutput.permissionDecision allow | deny | ask | defer, permissionDecisionReason, updatedInput (replaces the whole input), additionalContext. Precedence across hooks: deny > defer > ask > allow.

Handler types: command, http, mcp_tool, prompt, agent

Note: "allow" skips the prompt but never overrides deny or ask rules. A deny from a hook blocks even in bypassPermissions mode. @file references in your prompt don't fire it.

  • tool_name — e.g. Bash, Edit, Write, mcp__github__create_issue
  • tool_input — The tool's arguments — file_path is always absolute
  • tool_use_id — ID of this call
  • mcp_server — For MCP tools: { name, source }
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project",
  "permission_mode": "default",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test",
    "description": "Run test suite",
    "timeout": 120000,
    "run_in_background": false
  },
  "tool_use_id": "toolu_01ABC123"
}
PermissionRequest— Claude Code is about to show you a permission prompt

Matcher: Tool name — Bash, ExitPlanMode, Edit|Write, mcp__.*

Exit 2: Not honored — the permission flow proceeds unchanged.

JSON output: JSON: hookSpecificOutput.decision.behavior allow | deny, plus updatedInput / updatedPermissions (allow) or message / interrupt (deny)

Handler types: command, http, mcp_tool, prompt

Note: Keep the matcher narrow: an empty matcher that returns allow approves every prompt.

  • tool_name — The tool asking for permission
  • tool_input — Its arguments (no tool_use_id)
  • permission_suggestions — The rule / mode updates Claude Code suggests
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project",
  "permission_mode": "default",
  "hook_event_name": "PermissionRequest",
  "tool_name": "Bash",
  "tool_input": {
    "command": "rm -rf node_modules",
    "description": "Remove node_modules directory"
  },
  "permission_suggestions": [
    {
      "type": "addRules",
      "rules": [
        {
          "toolName": "Bash",
          "ruleContent": "rm -rf node_modules"
        }
      ],
      "behavior": "allow",
      "destination": "localSettings"
    }
  ]
}
PermissionDenied— auto mode denies a tool call (not manual denials, deny rules, or hook blocks)

Matcher: Tool name — Bash, *

Exit 2: Ignored — the denial already happened.

JSON output: JSON: hookSpecificOutput.retry true tells the model it may retry

Handler types: command, http, mcp_tool, prompt, agent

  • tool_name — The denied tool
  • tool_input — Its arguments
  • tool_use_id — ID of the call
  • reason — Why auto mode denied it
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project",
  "permission_mode": "auto",
  "hook_event_name": "PermissionDenied",
  "tool_name": "Bash",
  "tool_input": {
    "command": "rm -rf /tmp/build",
    "description": "Clean build directory"
  },
  "tool_use_id": "toolu_01ABC123",
  "reason": "[Irreversible Local Destruction]"
}
PostToolUse— right after a tool call succeeds

Matcher: Tool name — Edit|Write, Bash, mcp__.*, *

Exit 2: Shows stderr to Claude — the tool already ran.

JSON output: JSON: decision "block" + reason (feedback next to the result), hookSpecificOutput.additionalContext, updatedToolOutput (replaces what Claude sees)

Handler types: command, http, mcp_tool, prompt, agent

Note: Doesn't fire when Bash or another process rewrites the file — use FileChanged for that.

  • tool_name — The tool that ran
  • tool_input — Its arguments
  • tool_response — The tool's result — shape depends on the tool
  • tool_use_id — ID of the call
  • duration_ms — Execution time, when available
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project",
  "permission_mode": "default",
  "hook_event_name": "PostToolUse",
  "tool_name": "Write",
  "tool_input": {
    "file_path": "/Users/you/my-project/src/index.ts",
    "content": "export const hello = 'world'\n"
  },
  "tool_response": {
    "filePath": "/Users/you/my-project/src/index.ts",
    "type": "create"
  },
  "tool_use_id": "toolu_01ABC123",
  "duration_ms": 12
}
PostToolUseFailure— a tool that started running fails (not validation errors or permission denials)

Matcher: Tool name — Bash, *

Exit 2: Shows stderr to Claude — the tool already failed.

JSON output: JSON: hookSpecificOutput.additionalContext

Handler types: command, http, mcp_tool, prompt, agent

  • tool_name — The tool that failed
  • tool_input — Its arguments
  • error — What went wrong — for Bash, starts with Exit code N
  • is_interrupt — True when it was aborted
  • duration_ms — Execution time
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project",
  "permission_mode": "default",
  "hook_event_name": "PostToolUseFailure",
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test",
    "description": "Run test suite"
  },
  "tool_use_id": "toolu_01ABC123",
  "error": "Exit code 1\nError: Cannot find module 'express'",
  "is_interrupt": false,
  "duration_ms": 4187
}
PostToolBatch— every tool call in a parallel batch has resolved, before the next model call

Matcher: Not supported; fires on every occurrence

Exit 2: Stops the agentic loop before the next model call.

JSON output: JSON: hookSpecificOutput.additionalContext (once per batch); decision "block" or continue false stops the loop

Handler types: command, http, mcp_tool, prompt, agent

  • tool_calls — Array of { tool_name, tool_input, tool_use_id, tool_response } — serialized as the model sees it
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project",
  "permission_mode": "default",
  "hook_event_name": "PostToolBatch",
  "tool_calls": [
    {
      "tool_name": "Read",
      "tool_input": {
        "file_path": "/Users/you/my-project/ledger/accounts.py"
      },
      "tool_use_id": "toolu_01",
      "tool_response": "     1\tfrom __future__ import annotations\n"
    }
  ]
}

Subagents, tasks, teams

SubagentStart— a subagent is spawned or resumed

Matcher: Agent type — general-purpose, Explore, Plan, your-agent-name

Exit 2: Shows stderr in the subagent's transcript only.

JSON output: JSON: hookSpecificOutput.additionalContext is injected into the subagent

Handler types: command, http, mcp_tool

  • agent_id — Unique ID of the subagent
  • agent_type — Agent name (the matcher field)
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project",
  "hook_event_name": "SubagentStart",
  "agent_id": "agent-abc123",
  "agent_type": "Explore"
}
SubagentStop— a subagent finishes responding

Matcher: Agent type — general-purpose, Explore, Plan, your-agent-name

Exit 2: Keeps the subagent running; stderr becomes its next instruction.

JSON output: Same as Stop: decision "block" + reason, or hookSpecificOutput.additionalContext

Handler types: command, http, mcp_tool, prompt, agent

  • stop_hook_active — True if already continuing because of a stop hook
  • agent_id — Subagent ID
  • agent_type — Agent name (the matcher field)
  • agent_transcript_path — The subagent's own transcript
  • last_assistant_message — The subagent's final text
  • background_tasks / session_crons — In-flight work of the parent session
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project",
  "permission_mode": "default",
  "hook_event_name": "SubagentStop",
  "stop_hook_active": false,
  "agent_id": "def456",
  "agent_type": "Explore",
  "agent_transcript_path": "/Users/you/.claude/projects/my-project/abc123/subagents/agent-def456.jsonl",
  "last_assistant_message": "Analysis complete. Found 3 potential issues...",
  "background_tasks": [],
  "session_crons": []
}
TaskCreated— a task is being created with the TaskCreate tool

Matcher: Not supported; fires on every occurrence

Exit 2: Rolls back the task; stderr goes to Claude as the tool error.

JSON output: JSON: decision "block" + reason cancels the task (continue false is ignored)

Handler types: command, http, mcp_tool, prompt, agent

  • task_id — Task identifier
  • task_subject — Task title
  • task_description — Details, may be absent
  • teammate_name — Creating teammate, may be absent
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project",
  "hook_event_name": "TaskCreated",
  "task_id": "task-001",
  "task_subject": "Implement user authentication",
  "task_description": "Add login and signup endpoints"
}
TaskCompleted— a task is being marked completed

Matcher: Not supported; fires on every occurrence

Exit 2: The task stays open; stderr goes back to the model.

JSON output: Exit 2, or continue false + stopReason (stops a teammate; ignored when TaskUpdate triggered it)

Handler types: command, http, mcp_tool, prompt, agent

  • task_id — Task identifier
  • task_subject — Task title
  • task_description — Details, may be absent
  • teammate_name — Completing teammate, may be absent
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project",
  "permission_mode": "default",
  "hook_event_name": "TaskCompleted",
  "task_id": "task-001",
  "task_subject": "Implement user authentication"
}
TeammateIdle— an agent team teammate is about to go idle

Matcher: Not supported; fires on every occurrence

Exit 2: The teammate keeps working with stderr as feedback.

JSON output: Exit 2, or continue false + stopReason to stop the teammate

Handler types: command, http, mcp_tool, prompt, agent

  • teammate_name — The teammate going idle
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project",
  "permission_mode": "default",
  "hook_event_name": "TeammateIdle",
  "teammate_name": "researcher"
}

End of turn

Stop— the main agent finishes responding (not on your interrupt; API errors fire StopFailure)

Matcher: Not supported; fires on every occurrence

Exit 2: Claude keeps working; stderr becomes the reason to continue.

JSON output: JSON: decision "block" + reason (required), or hookSpecificOutput.additionalContext for non-error feedback. Capped at 8 consecutive blocks.

Handler types: command, http, mcp_tool, prompt, agent

  • stop_hook_active — True when already continuing because of a stop hook — check it to avoid loops
  • last_assistant_message — Claude's final text this turn
  • background_tasks — In-flight shells, subagents, monitors…
  • session_crons — Scheduled wakeups from /loop and cron tools
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project",
  "permission_mode": "default",
  "hook_event_name": "Stop",
  "stop_hook_active": false,
  "last_assistant_message": "I've completed the refactoring. Here's a summary...",
  "background_tasks": [],
  "session_crons": []
}
StopFailure— the turn ends because of an API error

Matcher: Error type — rate_limit, overloaded, authentication_failed, oauth_org_not_allowed, account_on_hold, billing_error, invalid_request, model_not_found, server_error, max_output_tokens, cloud_credential_error, unknown

Exit 2: Output and exit code are ignored (except terminalSequence).

JSON output: None — logging and alerts only

Handler types: command, http, mcp_tool

Note: Its matcher only treats letters, digits, _ and | as exact; anything else is a regex.

  • error — Error type (the matcher field)
  • error_details — More detail, when available
  • last_assistant_message — The rendered API error text
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project",
  "hook_event_name": "StopFailure",
  "error": "rate_limit",
  "error_details": "429 Too Many Requests",
  "last_assistant_message": "API Error: Rate limit reached"
}

Context and model

InstructionsLoaded— a CLAUDE.md or .claude/rules/*.md file loads into context — at start and when loaded lazily

Matcher: Load reason — session_start, nested_traversal, path_glob_match, include, compact

Exit 2: Exit code is ignored.

JSON output: None — observability only; JSON output is discarded

Handler types: command, http, mcp_tool

  • file_path — Absolute path of the loaded file
  • memory_type — User, Project, Local, or Managed
  • load_reason — Why it loaded (the matcher field)
  • globs — paths: patterns, for path_glob_match loads
  • trigger_file_path — File whose access triggered a lazy load
  • parent_file_path — Including file, for include loads
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project",
  "hook_event_name": "InstructionsLoaded",
  "file_path": "/Users/you/my-project/CLAUDE.md",
  "memory_type": "Project",
  "load_reason": "session_start"
}
PreCompact— right before context compaction

Matcher: What triggered compaction — manual, auto

Exit 2: Blocks compaction (for manual /compact, stderr is shown to you).

JSON output: JSON: decision "block"

Handler types: command, http, mcp_tool

  • trigger — manual or auto
  • custom_instructions — What you passed to /compact, or null
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project",
  "hook_event_name": "PreCompact",
  "trigger": "manual",
  "custom_instructions": null
}
PostCompact— after compaction completes

Matcher: What triggered compaction — manual, auto

Exit 2: Shows stderr to you only.

JSON output: None

Handler types: command, http, mcp_tool

  • trigger — manual or auto
  • compact_summary — The generated summary
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project",
  "hook_event_name": "PostCompact",
  "trigger": "manual",
  "compact_summary": "Summary of the compacted conversation..."
}
PreModelSwitch— before a model switch you or a client requested (/model, the picker, /config)

Matcher: Canonical name of the target model — claude-opus-5, claude-opus-4-6|claude-opus-5, .*opus.*

Exit 2: Blocks the switch and shows stderr to you.

JSON output: JSON: hookSpecificOutput.permissionDecision allow | deny | ask, or decision "block". A timeout also blocks.

Handler types: command, http, mcp_tool

Note: Requires v2.1.251+. Default timeout 30 seconds.

  • from_model / to_model — Model IDs
  • requested_model — Alias or ID requested, or null
  • source — command, picker, or sdk
  • context_tokens — Tokens the next request re-sends
  • prompt_cache_warm / cache_ttl — Cache state
  • estimated_cache_write_usd / pricing — Cost estimate of switching
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project",
  "hook_event_name": "PreModelSwitch",
  "from_model": "claude-sonnet-5",
  "to_model": "claude-opus-5",
  "requested_model": "opus",
  "source": "command",
  "context_tokens": 182340,
  "prompt_cache_warm": true,
  "cache_ttl": "5m",
  "estimated_cache_write_usd": 1.1396,
  "pricing": "catalog"
}
PostModelSwitch— after the session's model changes, including automatic fallbacks and resume

Matcher: Canonical name of the new model — .*opus.*, claude-sonnet-5

Exit 2: Shows stderr to you only.

JSON output: Plain stdout or hookSpecificOutput.additionalContext reaches Claude with the next request

Handler types: command, http, mcp_tool

Note: Requires v2.1.251+. Default timeout 30 seconds.

  • same as PreModelSwitch — Plus source auto or resume
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project",
  "hook_event_name": "PostModelSwitch",
  "from_model": "claude-sonnet-5",
  "to_model": "claude-opus-5",
  "requested_model": "opus",
  "source": "command",
  "context_tokens": 182340,
  "prompt_cache_warm": true,
  "cache_ttl": "5m",
  "estimated_cache_write_usd": 1.1396,
  "pricing": "catalog"
}

Files, directories, config

ConfigChange— a settings, managed policy, or skill file changes during a session

Matcher: Configuration source — user_settings, project_settings, local_settings, policy_settings, skills

Exit 2: Blocks the change from applying (never for policy_settings); no message is shown.

JSON output: JSON: decision "block"

Handler types: command, http, mcp_tool

  • source — Which kind of config changed (the matcher field)
  • file_path — The file that changed
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project",
  "hook_event_name": "ConfigChange",
  "source": "project_settings",
  "file_path": "/Users/you/my-project/.claude/settings.json"
}
CwdChanged— the working directory changes, e.g. Claude runs cd

Matcher: Not supported; fires on every occurrence

Exit 2: Shows stderr to you only.

JSON output: JSON: watchPaths (sets FileChanged's dynamic watch list), systemMessage. Has access to $CLAUDE_ENV_FILE.

Handler types: command, http, mcp_tool

  • old_cwd — Previous directory
  • new_cwd — New directory
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project/src",
  "hook_event_name": "CwdChanged",
  "old_cwd": "/Users/you/my-project",
  "new_cwd": "/Users/you/my-project/src"
}
DirectoryAdded— a working directory is added mid-session with /add-dir (or the SDK)

Matcher: How it was added — slash_command, register_repo_root

Exit 2: Stderr goes to the debug log; the directory is already added.

JSON output: None — runs in the background

Handler types: command, http, mcp_tool

  • directory — Absolute path added
  • source — slash_command or register_repo_root
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project",
  "hook_event_name": "DirectoryAdded",
  "directory": "/Users/you/my-other-repo",
  "source": "slash_command"
}
FileChanged— a watched file changes on disk, whatever changed it

Matcher: Literal filenames to watch, split on | — .envrc|.env, package.json, data.csv

Exit 2: Shows stderr to you only.

JSON output: JSON: watchPaths, systemMessage. Has access to $CLAUDE_ENV_FILE.

Handler types: command, http, mcp_tool

Note: The matcher is a list of literal filenames in the working directory, not a regex.

  • file_path — Absolute path of the changed file
  • event — change, add, or unlink
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project",
  "hook_event_name": "FileChanged",
  "file_path": "/Users/you/my-project/.envrc",
  "event": "change"
}
WorktreeCreate— a worktree is created (--worktree, isolation: worktree, background sessions)

Matcher: Not supported; fires on every occurrence

Exit 2: Any non-zero exit fails worktree creation.

JSON output: Print the new worktree's path as the last stdout line (HTTP hooks return worktreePath)

Handler types: command, http, mcp_tool

Note: Configuring it replaces the default Git worktree behavior entirely — meant for SVN, Perforce, Mercurial.

  • name — Slug for the new worktree
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project",
  "hook_event_name": "WorktreeCreate",
  "name": "feature-auth"
}
WorktreeRemove— a worktree is removed at session exit, subagent finish, or background-session delete

Matcher: Not supported; fires on every occurrence

Exit 2: Any non-zero exit fails removal if the directory still exists.

JSON output: Exit code only; JSON output is discarded

Handler types: command, http, mcp_tool

  • worktree_path — Absolute path being removed
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project",
  "hook_event_name": "WorktreeRemove",
  "worktree_path": "/Users/you/my-project/.claude/worktrees/feature-auth"
}

MCP elicitation

Elicitation— an MCP server asks for user input during a tool call

Matcher: MCP server name — your mcp server names

Exit 2: Denies the elicitation (stderr isn't shown).

JSON output: JSON: hookSpecificOutput.action accept | decline | cancel, content (form values)

Handler types: command, http, mcp_tool

  • mcp_server_name — The server (the matcher field)
  • message — What it's asking
  • mode — form or url
  • url / requested_schema / elicitation_id — Mode-specific details
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project",
  "hook_event_name": "Elicitation",
  "mcp_server_name": "my-mcp-server",
  "message": "Please provide your credentials",
  "mode": "form",
  "requested_schema": {
    "type": "object",
    "properties": {
      "username": {
        "type": "string",
        "title": "Username"
      }
    }
  }
}
ElicitationResult— you answered an MCP elicitation, before the answer goes back to the server

Matcher: MCP server name — your mcp server names

Exit 2: Blocks the response — the action becomes decline.

JSON output: JSON: hookSpecificOutput.action and content override your answer

Handler types: command, http, mcp_tool

  • mcp_server_name — The server
  • action — accept, decline, or cancel
  • content / mode / elicitation_id — The answer and details
{
  "session_id": "abc123",
  "transcript_path": "/Users/you/.claude/projects/my-project/00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/you/my-project",
  "hook_event_name": "ElicitationResult",
  "mcp_server_name": "my-mcp-server",
  "action": "accept",
  "content": {
    "username": "alice"
  },
  "mode": "form",
  "elicitation_id": "elicit-123"
}

Matchers and the if field

  • "*", "", or no matcher matches everything.
  • Letters, digits, _, -, spaces, , and | only: an exact name or list — Edit|Write and Edit, Write are the same. Matching is case-sensitive.
  • Any other character makes it an unanchored JavaScript regex: ^Notebook, mcp__memory__.*. Edit.* also matches NotebookEdit — anchor with ^…$.
  • MCP tools are named mcp__<server>__<tool>. A bare mcp__memory is an exact string that matches no tool; use mcp__memory__.*.
  • Events without matcher support (UserPromptSubmit, Stop, PostToolBatch, TeammateIdle, TaskCreated, TaskCompleted, WorktreeCreate, WorktreeRemove, CwdChanged, MessageDisplay) silently ignore one.
  • FileChanged's matcher is a |-separated list of literal filenames to watch, not a regex.
  • if narrows a single handler with one permission rule — Bash(git *), Edit(*.ts) — and only on PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, and PermissionDenied. On any other event a handler with if never runs. It's best-effort, so enforce hard rules with permissions.

Hook types: command, http, mcp_tool, prompt, agent

TypeWhat it doesDefault timeout
commandRuns a shell command (sh -c on macOS/Linux); JSON on stdin, results via exit code + stdout. Add args for exec form without a shell; async: true runs it in the background600 s
httpPosts the JSON to url; the response body uses the same JSON output. Status codes can't block — return a 2xx with a decision. Headers can use $VARS listed in allowedEnvVars600 s
mcp_toolCalls tool on an already-connected MCP server; input supports ${tool_input.file_path}-style substitution600 s
promptSends your prompt ($ARGUMENTS = the input JSON) to a fast model, which answers {"ok": true} or {"ok": false, "reason": …}30 s
agentLike prompt, but a subagent that can read files and run tools for up to 50 turns. Experimental60 s

UserPromptSubmit, PreModelSwitch, and PostModelSwitch lower the command/http/mcp_tool default to 30 seconds, MessageDisplay to 10, and all SessionEnd hooks share a 1.5-second budget. SessionStart and Setup only run command and mcp_tool hooks; prompt and agent hooks work on the tool, prompt, stop, and task events.

Where hooks live

LocationScope
~/.claude/settings.jsonAll your projects, this machine
.claude/settings.jsonOne project — commit it to share with the team
.claude/settings.local.jsonOne project, just you
Managed policy settingsOrganization-wide, set by admins
Plugin hooks/hooks.jsonWhile the plugin is enabled
Skill or subagent frontmatterWhile that skill or subagent is active

Hooks from all of these merge rather than override. Reference project scripts as "$CLAUDE_PROJECT_DIR"/.claude/hooks/my-hook.sh so they resolve no matter where Claude has cd'd, and chmod +x them. /hooks shows a read-only list of everything registered and which file it came from; "disableAllHooks": true turns them all off.

Claude Code hooks examples

The recipes in the builder cover the common cases. For anything longer than a line, move the logic into a script — this is the protected-files example from the official guide, saved as .claude/hooks/protect-files.sh:

#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
FILE_PATH="${FILE_PATH//\\//}"   # normalize windows separators

PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")
for pattern in "${PROTECTED_PATTERNS[@]}"; do
  if [[ "$FILE_PATH" == *"$pattern"* ]]; then
    echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
    exit 2
  fi
done
exit 0

Register it with a PreToolUse hook on Edit|Write whose command is "$CLAUDE_PROJECT_DIR"/.claude/hooks/protect-files.sh. And a Stop hook that blocks must check stop_hook_active first, or Claude can keep being sent back — Claude Code gives up after eight consecutive blocks:

#!/bin/bash
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0   # already continued once: let claude stop
fi
# ... your check; exit 2 with a reason on stderr to keep claude working

Security and debugging

  • Hooks run with your full user permissions. Review every command, quote shell variables ("$VAR"), and skip sensitive paths.
  • Workspace trust: in an interactive session, hooks wait until you accept the trust dialog for the folder. claude -p and SDK runs treat the folder as trusted — review a repo's .claude/ before scripting against it, or pass --settings '{"disableAllHooks": true}'.
  • Debug: start with claude --debug-file /tmp/claude.log and tail -f it to see which hooks matched, their exit codes, stdout, and stderr. Ctrl+O opens the transcript view.
  • JSON ignored? A field at the wrong level (permissionDecision belongs inside hookSpecificOutput) is dropped silently, and anything printed before the JSON makes Claude Code treat it as plain text.

Keep going

Hooks enforce; commands and skills instruct. Build a reusable prompt like /commit or /review-pr with the Claude Code slash command generator, and write the project context Claude reads every session with the CLAUDE.md generator. To compare two versions of a settings file, use the JSON diff checker.

Frequently asked questions

What are hooks in Claude Code?

Hooks are commands you configure in settings.json that Claude Code runs automatically at points in its lifecycle — before a tool call, after a file edit, when Claude stops, when a session starts. They're deterministic: a formatter hook runs after every edit whether or not the model remembers to. Besides shell commands, a hook can post to a URL, call an MCP tool, or ask a Claude model (prompt and agent hooks).

What hook events does Claude Code support in 2026?

The hooks reference lists SessionStart, Setup, InstructionsLoaded, UserPromptSubmit, UserPromptExpansion, MessageDisplay, PreToolUse, PermissionRequest, PermissionDenied, PostToolUse, PostToolUseFailure, PostToolBatch, Notification, SubagentStart, SubagentStop, TaskCreated, TaskCompleted, Stop, StopFailure, TeammateIdle, ConfigChange, CwdChanged, DirectoryAdded, FileChanged, WorktreeCreate, WorktreeRemove, PreCompact, PostCompact, PreModelSwitch, PostModelSwitch, SessionEnd, Elicitation, and ElicitationResult — 33 events. The table on this page lists when each fires and whether it can block.

Where do I put Claude Code hooks?

In a settings file, under a top-level "hooks" key: ~/.claude/settings.json applies to all your projects, .claude/settings.json is shared with the repo, and .claude/settings.local.json is just for you in one project. Plugins, skill frontmatter, subagent frontmatter, and managed policy settings can define hooks too. Hooks from every level are merged, not overridden.

How does a hook block an action?

Exit with code 2 and write the reason to stderr. On events that can block — PreToolUse, UserPromptSubmit, Stop, SubagentStop, PreCompact, ConfigChange and others — that stops the action, and for PreToolUse and Stop Claude sees your stderr as the reason. Exit 1 doesn't block: it's a non-blocking error and the action proceeds. For finer control, exit 0 and print JSON, e.g. hookSpecificOutput.permissionDecision "deny" on PreToolUse.

What JSON does a hook receive?

Every hook gets session_id, transcript_path, cwd, and hook_event_name on stdin (HTTP hooks get it as the POST body), plus permission_mode and event-specific fields: tool_name, tool_input and tool_use_id for tool events, tool_response for PostToolUse, prompt for UserPromptSubmit, source for SessionStart, stop_hook_active and last_assistant_message for Stop. The test panel above shows the exact sample for any event.

What's the difference between matcher and if?

Matcher filters a whole group of handlers by one field — the tool name for tool events, the source for SessionStart, the notification type for Notification. If is set on a single handler, only works on tool events, and uses permission-rule syntax to look at arguments too, like Bash(git *) or Edit(*.ts), so the process isn't even spawned for other calls.

How do I auto-format files after Claude edits them?

Add a PostToolUse hook with the matcher Edit|Write and a command that reads the edited path from stdin, e.g. jq -r '.tool_input.file_path' | xargs npx prettier --write. The recipes above include Prettier, Ruff, and gofmt versions. PostToolUse doesn't fire when a Bash command rewrites a file — use a FileChanged hook for that.

How do I get a notification when Claude Code needs input?

Add a Notification hook in ~/.claude/settings.json. On macOS: osascript -e 'display notification "Claude Code needs your attention" with title "Claude Code"'; on Linux: notify-send. permission_prompt fires after a prompt has waited about six seconds, and idle_prompt about 60 seconds after Claude finishes; use PermissionRequest for an instant signal.

Why isn't my hook running?

Run /hooks to check it's registered under the right event. Matchers are case-sensitive (Bash, not bash). Make sure the settings file is valid JSON — no comments or trailing commas — and that script files are executable. The if field on a non-tool event stops the hook from ever running. In an interactive session, hooks wait until you accept the workspace trust dialog. claude --debug-file /tmp/claude.log records every match, exit code, stdout and stderr.

How do I disable hooks?

Set "disableAllHooks": true in a settings file, or pass --settings '{"disableAllHooks": true}' for one run. There's no switch for a single hook — remove its entry instead. Managed hooks can only be disabled from managed settings.

Related free tools

See all free tools →

Built something? Put it online in seconds

host0 is the cloud for small software: bring any coding agent, build the tool only you need — like this one — and say "deploy to host0". Live at a shareable URL, no servers to run.