Vibepanions Get Vibepanions

Home / Guides / Claude Code hooks

Claude Code hooks: get an alert when a session stops or needs you

Updated 23 Sep 2026 by DevSparks

Quick answer: hooks are shell commands (or HTTP calls, MCP tools, prompts or agents) that Claude Code runs at fixed points in a session. Add a hooks block to ~/.claude/settings.json. Use the Notification event to hear when Claude needs permission or input, Stop to hear when it finishes, PreToolUse to block a command before it runs, and PostToolUse to run a formatter after an edit. Each hook gets the event as JSON on stdin; exit 2 blocks, exit 0 lets things carry on.

What are Claude Code hooks?

Hooks are handlers you define that Claude Code runs automatically at specific points in its lifecycle. The docs list five handler types: command (a shell command), http (a POST to a URL), mcp_tool (a tool on a connected MCP server), prompt (a single-turn check by a Claude model) and agent (a subagent that can read files before deciding; experimental). This guide uses command hooks, the simplest kind.

Claude Code fires the same events in the terminal, the IDE extensions, the Desktop app and cloud sessions. When several hooks match an event, they run in parallel.

Which hook events are there?

The reference lists more than thirty events. These are the ones most people start with, and the fields each receives on top of the common ones (session_id, transcript_path, cwd, hook_event_name and, on many events, permission_mode):

EventWhen it firesExtra inputMatcher filters
SessionStartA session begins or resumessource, sometimes modelstartup, resume, clear, compact, fork
UserPromptSubmitYou submit a prompt, before Claude processes itpromptNo matcher
PreToolUseBefore a tool call runs; can block ittool_name, tool_input, tool_use_idTool name
PermissionRequestClaude Code is about to ask you for permissiontool_name, tool_input, permission_suggestionsTool name
PostToolUseAfter a tool call succeedstool_input, tool_response, duration_msTool name
NotificationClaude Code sends a notificationmessage, title, notification_typepermission_prompt, idle_prompt and others
StopClaude finishes responding (not on a user interrupt)stop_hook_active, last_assistant_message, background_tasks, session_cronsNo matcher
StopFailureThe turn ends on an API errorerror, error_detailsError type, such as rate_limit
SubagentStopA subagent finishesagent_id, agent_type, last_assistant_messageAgent type
SessionEndA session endsreasonclear, resume, logout, prompt_input_exit, other

The rest cover more specialised moments: Setup, UserPromptExpansion, PermissionDenied, PostToolUseFailure, PostToolBatch, MessageDisplay, SubagentStart, TaskCreated, TaskCompleted, TeammateIdle, InstructionsLoaded, ConfigChange, CwdChanged, DirectoryAdded, FileChanged, WorktreeCreate, WorktreeRemove, PreCompact, PostCompact, PreModelSwitch, PostModelSwitch, Elicitation and ElicitationResult.

A matcher made only of letters, digits, _, -, spaces, commas and | is an exact match or a list, so Edit|Write matches either tool. Anything else is treated as a JavaScript regular expression. Matchers are case-sensitive, and "*", "" or no matcher matches everything.

Where do you configure hooks?

Hooks live in JSON settings files, nested three levels deep: the event, a matcher group, then one or more handlers. Where you put them sets the scope:

Hooks from different levels add up rather than replace each other. Type /hooks in Claude Code to see every configured hook and which file it came from. The menu is read-only, so edit the JSON to change anything.

What does a hook receive, and how does it answer?

A command hook gets the event as one JSON object on stdin. A Notification hook, for example, receives:

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf.jsonl",
  "cwd": "/Users/...",
  "hook_event_name": "Notification",
  "message": "Claude needs your permission",
  "title": "Permission needed",
  "notification_type": "permission_prompt"
}

It answers with its exit code, and optionally JSON on stdout:

For finer control, exit 0 and print JSON. PreToolUse uses hookSpecificOutput.permissionDecision with allow, deny, ask or defer. PostToolUse and Stop use a top-level decision of "block" with a reason. Command hooks time out after 600 seconds unless you set timeout.

Example 1: a Mac notification when Claude needs you

The Notification event fires with permission_prompt when a permission prompt has waited about six seconds, and idle_prompt about 60 seconds after Claude finishes and you have not typed. This script shows the message Claude Code sent, with the project folder in the title. It needs jq (brew install jq).

~/.claude/hooks/notify.sh

#!/bin/bash
# Notification hook: show a macOS notification with the text Claude Code sent.
input=$(cat)
msg=$(jq -r '.message // "Claude Code needs you"' <<<"$input")
project=$(basename "$(jq -r '.cwd // empty' <<<"$input")")
# Pass text as arguments so quotes in the message cannot break the script.
osascript -e 'on run argv' \
  -e 'display notification (item 1 of argv) with title (item 2 of argv)' \
  -e 'end run' "$msg" "Claude Code ${project}" >/dev/null 2>&1
exit 0

~/.claude/settings.json

{
  "hooks": {
    "Notification": [
      {
        "matcher": "permission_prompt|idle_prompt",
        "hooks": [
          { "type": "command", "command": "\"$HOME/.claude/hooks/notify.sh\"", "timeout": 10 }
        ]
      }
    ]
  }
}

Run chmod +x ~/.claude/hooks/notify.sh. If nothing appears, the docs explain that osascript notifications go through Script Editor: run osascript -e 'display notification "test"' once, then allow notifications for Script Editor in System Settings > Notifications.

Example 2: a sound when Claude stops

Stop fires every time Claude finishes responding, not only when a whole task is done, and never on an interrupt. A short sound is enough. "async": true runs it in the background so nothing waits on it.

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "afplay /System/Library/Sounds/Glass.aiff", "async": true }
        ]
      }
    ]
  }
}

Want a banner instead? Point the same event at notify.sh; it falls back to "Claude Code needs you" because Stop input has no message. Do not exit 2 from a Stop hook unless you mean it: that tells Claude to keep going.

Example 3: block a risky command

A PreToolUse hook on Bash sees the command before it runs. Exit 2 blocks the call and Claude reads your stderr as the reason.

~/.claude/hooks/block-risky.sh

#!/bin/bash
# PreToolUse hook for Bash: refuse a few commands that are hard to undo.
input=$(cat)
cmd=$(jq -r '.tool_input.command // empty' <<<"$input")
if grep -Eq 'rm[[:space:]]+-[a-zA-Z]*[rR][a-zA-Z]*f?[[:space:]]+(/|~|\$HOME)([[:space:]]|$)|git[[:space:]]+push[[:space:]].*(--force|-f)([[:space:]]|$)|git[[:space:]]+reset[[:space:]]+--hard' <<<"$cmd"; then
  echo "Blocked by PreToolUse hook: this command is on the risky list. Ask the user to run it." >&2
  exit 2
fi
exit 0
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "\"$HOME/.claude/hooks/block-risky.sh\"", "timeout": 5 }
        ]
      }
    ]
  }
}

Treat this as a seatbelt, not a lock. A pattern list will miss creative spellings, and the docs note that a PreToolUse command hook that times out does not block. For hard rules, add deny rules to your permissions as well.

Example 4: run a formatter after edits

PostToolUse with an Edit|Write matcher runs after Claude changes a file. This script formats the file with Prettier if the project has it installed, and does nothing otherwise.

~/.claude/hooks/format.sh

#!/bin/bash
# PostToolUse hook for Edit|Write: format the file Claude just changed.
input=$(cat)
file=$(jq -r '.tool_input.file_path // empty' <<<"$input")
[ -f "$file" ] || exit 0
case "$file" in
  *.js|*.jsx|*.ts|*.tsx|*.css|*.json|*.md)
    npx --no-install prettier --write "$file" >/dev/null 2>&1 ;;
esac
exit 0
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "\"$HOME/.claude/hooks/format.sh\"", "timeout": 30 }
        ]
      }
    ]
  }
}

A PostToolUse hook cannot undo anything; the edit already happened. If you add more than one event, put them side by side inside the single hooks object.

How do you keep hooks safe and fast?

Vibepanions' own hook follows the same pattern. It reads stdin fully, exits at once if the app is not running, sends one line to a local socket in the background with a one-second limit, never writes to stdout and always exits 0. Its timeout is 5 seconds on most events and 1 on SessionEnd.

Hooks for every session at once

A notification per event works for one session, but with five terminals you still have to work out which one pinged you. Vibepanions registers its own hooks for the session events, shows you the exact diff to settings.json and backs the file up before installing, and can uninstall them. It then shows every Claude Code session as a desk in one window: green while working, amber and a waving character when any session needs you, and a click takes you to that terminal.

FAQ

Why does my Notification hook fire late?

By design. In a terminal session, permission_prompt fires once the prompt has waited about six seconds without you typing, and idle_prompt about 60 seconds after Claude finishes. For a signal the moment Claude asks for permission, use a PermissionRequest hook.

Why does my PreToolUse hook not block anything?

Check the exit code. Only exit 2, or a JSON permissionDecision of deny, blocks a tool call. Exit 1 is a non-blocking error and the call goes ahead. A hook that times out does not block either.

Do I need to restart Claude Code after editing hooks?

Usually not. The file watcher normally picks up direct edits to settings files. If a hook does not show in /hooks after a few seconds, restart the session.

How do I turn all hooks off?

Set "disableAllHooks": true in your settings file, or pass --settings '{"disableAllHooks": true}' for one run. Hooks from managed settings keep running unless it is also set there.

Can a hook see my prompts and code?

Yes. A hook receives the event's JSON, which can include your prompt, tool inputs such as file contents, and the path to the transcript. It also runs with your full user permissions, so only install hooks you have read.

Want every session's state on screen? Get Vibepanions. SGD 7.99 once, full refund within 14 days.

Sources

Fetched 23 Sep 2026.

Next: Claude Code usage monitors for Mac, compared.