AC Bridge

Claude Code hooks: every event, plus working examples

Last updated

The short answer A Claude Code hook is a shell command that Claude Code runs at a set point in its lifecycle: before a tool runs, after a file edit, when it finishes, when it needs you. You add hooks to a settings file such as ~/.claude/settings.json under a hooks key, and the table below lists every event you can hook.

Checked against the Claude Code hooks reference and hooks guide on .

On this page: where hooks go · every hook event · matchers · input and output · examples · hooks not working · questions · a light instead.

Where Claude Code hooks go: settings.json

Hooks live in settings files. Which file you pick decides who they apply to.

FileScopeShared?
~/.claude/settings.jsonAll your projectsNo, local to your machine
.claude/settings.jsonOne projectYes, can be committed to the repo
.claude/settings.local.jsonOne projectNo, gitignored when Claude Code saves a setting to it

Managed policy settings, plugins, skills and subagents can also carry hooks; this page sticks to the three files above. The JSON has three levels: the event name, then a matcher group, then the list of hooks to run.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/lint-check.sh"
          }
        ]
      }
    ]
  }
}

Every event name is a key inside the single hooks object, so to add a second event, put it beside the first rather than replacing the object. Claude Code watches your settings files and normally picks up hook edits without a restart.

Every Claude Code hook event

Claude Code lists these events today. "Can it block?" means a 2 exit code (see below) stops or changes what happens next. For the rest, exit 2 does not stop the action, though some can still act through JSON output: a PermissionRequest hook can deny the request, and "continue": false stops Claude on events that honor it.

EventWhen it firesCan it block?Typical use
SessionStartA session begins or resumesNoAdd context to the session
SetupYou start with --init-only, or --init or --maintenance in -p modeNoOne-time preparation in CI or scripts
UserPromptSubmitA prompt is submitted, before Claude processes itYesCheck or add to a prompt
UserPromptExpansionA typed command expands into a promptYesStop a command expanding
PreToolUseBefore a tool call runsYesBlock a command or an edit
PermissionRequestA tool call needs a permission decisionNo (exit 2 is not honored)Notify, or decide through JSON output
PermissionDeniedAuto mode denies a tool callNoLog denials
PostToolUseAfter a tool call succeedsNo (the tool already ran)Format files, log commands
PostToolUseFailureAfter a tool call failsNoLog failures
PostToolBatchA batch of parallel tool calls resolves, before the next model callYesCheck results once per batch
NotificationClaude Code sends a notificationNoDesktop alert when it needs you
MessageDisplayAssistant message text is displayedNoAdjust displayed text
SubagentStartA subagent is spawnedNoLog subagent use
SubagentStopA subagent finishesYesKeep a subagent going
TaskCreatedA task is being created with TaskCreateYesRoll back a task
TaskCompletedA task is being marked completedYesRequire a check first
StopClaude finishes respondingYesSound, or make Claude continue
StopFailureThe turn ends because of an API errorNoAlert on failures
TeammateIdleAn agent team teammate is about to go idleYesKeep a teammate working
InstructionsLoadedA CLAUDE.md or .claude/rules/*.md file is loadedNoAudit loaded instructions
ConfigChangeA configuration file changes during a sessionYesAudit or block settings changes
CwdChangedThe working directory changesNoReload environment (for example direnv)
DirectoryAddedA directory is added with /add-dirNoLog added directories
FileChangedA watched file changes on diskNoReact to .env or .envrc
WorktreeCreateA worktree is being createdYes (any non-zero exit fails it)Replace default worktree creation
WorktreeRemoveA worktree a hook created is being removedYes (a non-zero exit fails it if the directory still exists)Clean up
PreCompactBefore context compactionYesBlock compaction
PostCompactAfter context compaction completesNoLog compactions
PreModelSwitchBefore a requested model switch is appliedYesBlock a switch
PostModelSwitchAfter the session's model changesNoLog the new model
ElicitationAn MCP server asks for user input during a tool callYesDecline a request
ElicitationResultAfter you respond to an MCP elicitation, before it is sent backYesBlock the response
SessionEndA session terminatesNoClean up, log the session

Most hooks use "type": "command", which runs a shell command, and that is all this page uses. The docs also describe http, mcp_tool, prompt and agent hook types. The full schema for each event is in the hooks reference.

Hook matchers

Without a matcher, a hook fires on every occurrence of its event. A matcher narrows it. What it filters depends on the event:

A matcher that is omitted, "" or "*" matches everything. A matcher made only of letters, digits, _, -, spaces, commas and | is an exact name or a list, so Edit|Write matches those two tools. Any other character makes it a JavaScript regular expression. Matchers are case-sensitive. Events without matcher support, such as UserPromptSubmit and Stop, silently ignore a matcher.

Notification matchers

MatcherFires when
permission_promptClaude needs you to approve a tool use or a sandboxed command's network request, and the prompt has waited about six seconds
idle_promptClaude finished responding about 60 seconds ago and you have not typed since
auth_successAuthentication completes
elicitation_dialogAn MCP server opens a form and you have not typed for about six seconds
agent_needs_inputA background session starts waiting on your input while agent view is open in a terminal; also a teammate's setup question or auto mode's classifier-charges notice after about six seconds without typing
agent_completedA background session finishes or fails, only while agent view is open in a terminal

The docs list a few more (elicitation_url_dialog, elicitation_complete, elicitation_response and three quota_auto_resume_* types). In a terminal, each keystroke restarts the six-second wait for permission_prompt. In Claude Desktop and the VS Code extension (Claude Code 2.1.233 or later), it comes about six seconds after Claude asks, whether or not you type. The PermissionRequest event, by contrast, fires as soon as the permission dialog appears.

What a hook receives and returns

Input. Claude Code sends the event as JSON on stdin. Every event carries common fields such as session_id, transcript_path, cwd and hook_event_name, and most also carry permission_mode. Tool events add tool_name and tool_input. A Bash call, for example, has the command in tool_input.command, and an edit has the path in tool_input.file_path.

Exit codes. The exit code is the simplest way to answer:

JSON output. To do more than allow or block, exit 0 and print one JSON object. Universal fields include continue (set false to stop Claude entirely), stopReason and systemMessage (a warning shown to you). Events such as UserPromptSubmit, PostToolUse and Stop accept a top-level "decision": "block" with a reason. PreToolUse uses hookSpecificOutput instead:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Database writes are not allowed"
  }
}

permissionDecision takes allow, deny, ask or defer (defer works only in -p mode; interactive sessions ignore it). Choose one approach per hook: exit codes alone, or exit 0 with JSON. Fields placed at the top level that belong inside hookSpecificOutput are ignored without an error.

Claude Code hooks examples

Each example below goes in one of the files from the first section. The ones that read tool_input use jq to parse the JSON.

Format files after every edit (PostToolUse)

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ]
  }
}

Put it in .claude/settings.json. The matcher limits it to the two file-editing tools, and the command passes the edited path to Prettier. Claude can also change files with shell commands, which this matcher does not see.

Block edits to a protected file (PreToolUse)

Save this as .claude/hooks/protect-files.sh and run chmod +x on it:

#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty')
# Normalize Windows backslash separators so the patterns below match
FILE_PATH="${FILE_PATH//\\//}"

PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")

for pattern in "${PROTECTED_PATTERNS[@]}"; do
  if [[ "$FILE_PATH" == *"$pattern"* ]]; then
    printf 'Blocked: %s matches protected pattern %s\n' "$FILE_PATH" "$pattern" >&2
    exit 2
  fi
done

exit 0

Then register it in .claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
          }
        ]
      }
    ]
  }
}

Exit code 2 blocks the edit and sends your stderr message to Claude, so it can pick another approach. PreToolUse runs before any permission-mode check, so a deny from a hook holds even when permissions are bypassed.

A desktop notification when Claude Code is waiting (Notification)

An empty matcher fires on every notification type. On macOS:

{
  "hooks": {
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

On Linux, use "command": "notify-send 'Claude Code' 'Claude Code needs your attention'" (it needs a desktop notification daemon). On Windows, the docs use powershell.exe -Command "[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('Claude Code needs your attention', 'Claude Code')", which opens a dialog that can land behind your terminal. Set the matcher to permission_prompt to alert only on approval requests. The notifications guide covers more ways to get alerted.

A sound when Claude Code is done (Stop)

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

That is macOS; Linux and Windows need their own sound command, so use the one your system provides. Stop fires whenever Claude finishes responding, not only at the end of a task, and does not fire when you interrupt. It has no matcher, so none is needed. The notifications guide covers the terminal bell and desktop alerts on each system.

Add context at session start (SessionStart)

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "compact",
        "hooks": [
          {
            "type": "command",
            "command": "printf 'Reminder: use Bun, not npm. Run bun test before committing.'"
          }
        ]
      }
    ]
  }
}

Plain text a SessionStart command writes to stdout is added to Claude's context. The compact matcher re-injects it after every compaction; drop the matcher or use startup for a new session. Swap the printf for git log --oneline -5 to show recent commits.

Log every command Claude runs (PostToolUse)

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.command' >> ~/.claude/command-log.txt"
          }
        ]
      }
    ]
  }
}

PostToolUse fires after the command completes, so tool_input.command holds what ran. Each command is appended to the file as a line.

Claude Code hooks not working? Check these

printf '%s' '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh
printf 'exit code: %s\n' "$?"

Questions people ask

Where do I put Claude Code hooks?

In a hooks block in ~/.claude/settings.json for every project, .claude/settings.json for one shared project, or .claude/settings.local.json for one project on your machine only.

Can a hook stop Claude Code from running a command?

Yes. A PreToolUse hook can exit with 2, or return permissionDecision: "deny", to block the tool call. It fires before permission-mode checks, so it holds even with --dangerously-skip-permissions.

What is the difference between the Notification and Stop hooks?

Notification fires when Claude Code sends a notification, such as a permission prompt or an idle prompt. Stop fires every time Claude finishes responding, whether or not it needs anything from you. A finished-sound belongs on Stop; a "come back and approve" alert belongs on Notification with the permission_prompt matcher.

Do Claude Code hooks work in the VS Code extension?

Yes. The hooks reference says the same hook events fire in the IDE extensions. One difference: there, the permission_prompt notification comes about six seconds after Claude asks even while you type, where a terminal waits until you stop typing.

Can a hook turn on a light?

A command hook can run anything your machine can run, including a script that calls a smart-light API. You then own that script for each brand of light and each event; the last section shows a ready-made way.

Can a hook run a slash command?

Not a command hook. Command hooks communicate through stdout, stderr and exit codes only, so they can pass text back to Claude but cannot trigger / commands or make Claude call a tool. A separate mcp_tool hook type can call a tool on a configured MCP server.

How long can a hook run?

A command hook has a default timeout of 10 minutes, and some events lower it. Set the timeout field, in seconds, on a hook to change it.

Or let a light show it

A sound needs you at the desk and listening, and a desktop alert needs you looking at the screen. A light works from across the room, with headphones on, and adds no extra ping.

AC Bridge runs on these same Claude Code hooks and turns the smart lights and devices you already own (Hue, Govee, WLED, LIFX, Tapo, WiZ, Home Assistant and more) into a status light for each session: working, needs permission, finished. Its hooks come with the AC Bridge plugin for Claude Code, so there is nothing to paste into your settings files, and because Claude Code runs every matching hook, your own notification and format hooks keep working beside it. Needs permission follows Claude Code's permission_prompt notification. See how the Claude Code status light works and the setup docs.

One plan: a 3-day free trial with your card taken up front, then £2.99 a month, charged when the trial ends. Cancel any time from your account; cancel before the trial ends and you pay nothing. It works with the Claude Code or Codex you already run, on Windows, macOS or Linux. There is no API key to buy.

LAN devices are driven straight from your machine and their keys never leave it; your account, settings, device list, usage counts and session states sync through our servers. Your prompts, code and session transcripts are never collected. Privacy policy.

Try it on your own lights Start the 3-day free trial, or read the setup docs first.