Claude Code hooks: every event, plus working examples
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.
| File | Scope | Shared? |
|---|---|---|
~/.claude/settings.json | All your projects | No, local to your machine |
.claude/settings.json | One project | Yes, can be committed to the repo |
.claude/settings.local.json | One project | No, 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.
| Event | When it fires | Can it block? | Typical use |
|---|---|---|---|
SessionStart | A session begins or resumes | No | Add context to the session |
Setup | You start with --init-only, or --init or --maintenance in -p mode | No | One-time preparation in CI or scripts |
UserPromptSubmit | A prompt is submitted, before Claude processes it | Yes | Check or add to a prompt |
UserPromptExpansion | A typed command expands into a prompt | Yes | Stop a command expanding |
PreToolUse | Before a tool call runs | Yes | Block a command or an edit |
PermissionRequest | A tool call needs a permission decision | No (exit 2 is not honored) | Notify, or decide through JSON output |
PermissionDenied | Auto mode denies a tool call | No | Log denials |
PostToolUse | After a tool call succeeds | No (the tool already ran) | Format files, log commands |
PostToolUseFailure | After a tool call fails | No | Log failures |
PostToolBatch | A batch of parallel tool calls resolves, before the next model call | Yes | Check results once per batch |
Notification | Claude Code sends a notification | No | Desktop alert when it needs you |
MessageDisplay | Assistant message text is displayed | No | Adjust displayed text |
SubagentStart | A subagent is spawned | No | Log subagent use |
SubagentStop | A subagent finishes | Yes | Keep a subagent going |
TaskCreated | A task is being created with TaskCreate | Yes | Roll back a task |
TaskCompleted | A task is being marked completed | Yes | Require a check first |
Stop | Claude finishes responding | Yes | Sound, or make Claude continue |
StopFailure | The turn ends because of an API error | No | Alert on failures |
TeammateIdle | An agent team teammate is about to go idle | Yes | Keep a teammate working |
InstructionsLoaded | A CLAUDE.md or .claude/rules/*.md file is loaded | No | Audit loaded instructions |
ConfigChange | A configuration file changes during a session | Yes | Audit or block settings changes |
CwdChanged | The working directory changes | No | Reload environment (for example direnv) |
DirectoryAdded | A directory is added with /add-dir | No | Log added directories |
FileChanged | A watched file changes on disk | No | React to .env or .envrc |
WorktreeCreate | A worktree is being created | Yes (any non-zero exit fails it) | Replace default worktree creation |
WorktreeRemove | A worktree a hook created is being removed | Yes (a non-zero exit fails it if the directory still exists) | Clean up |
PreCompact | Before context compaction | Yes | Block compaction |
PostCompact | After context compaction completes | No | Log compactions |
PreModelSwitch | Before a requested model switch is applied | Yes | Block a switch |
PostModelSwitch | After the session's model changes | No | Log the new model |
Elicitation | An MCP server asks for user input during a tool call | Yes | Decline a request |
ElicitationResult | After you respond to an MCP elicitation, before it is sent back | Yes | Block the response |
SessionEnd | A session terminates | No | Clean 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:
- Tool events (
PreToolUse,PostToolUse,PostToolUseFailure,PermissionRequest,PermissionDenied): the tool name, such asBash,Edit|Writeormcp__.*. SessionStart: how the session started:startup,resume,clear,compactorfork.SessionEnd:clear,resume,logout,prompt_input_exitorother.PreCompactandPostCompact:manualorauto.ConfigChange:user_settings,project_settings,local_settings,policy_settingsorskills.Notification: the notification type, listed next.
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
| Matcher | Fires when |
|---|---|
permission_prompt | Claude needs you to approve a tool use or a sandboxed command's network request, and the prompt has waited about six seconds |
idle_prompt | Claude finished responding about 60 seconds ago and you have not typed since |
auth_success | Authentication completes |
elicitation_dialog | An MCP server opens a form and you have not typed for about six seconds |
agent_needs_input | A 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_completed | A 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:
0means success. Stdout is read as JSON when the whole output is one JSON object, and as plain text when it does not start with{; output that starts with{and ends with}but is not valid JSON is a parse error. ForUserPromptSubmit,UserPromptExpansion,SessionStartandPostModelSwitch, plain text on stdout is added to Claude's context.2is a blocking error on the events marked "Yes" above. The message is your stderr: on most of those events Claude sees it, onPreModelSwitchonly you do, andConfigChangeandElicitationshow no message.- Any other code, with plain text or nothing on stdout, is a non-blocking error: the action goes ahead and a "hook error" notice appears (if stdout is valid JSON, the exit code is ignored and the JSON decides). That includes
1, so a policy hook should useexit 2.
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
- Open
/hooks. Type it at the prompt to browse every configured hook by event. If yours is missing, it is not loaded. - Wait, then restart. File edits are normally picked up automatically. If the hook has not appeared after a few seconds, restart the session to force a reload.
- Trust the folder. In an interactive session, hooks from every settings file wait until you accept the folder's trust dialog. If
/hookssays only hooks from managed settings run here, an administrator has blocked your own hooks. - Check the JSON. Trailing commas and comments are not allowed. Confirm the file is
.claude/settings.jsonfor a project or~/.claude/settings.jsonfor all projects. - Match the case. Matchers are case-sensitive, so
bashdoes not matchBash. Check you used the right event too:PreToolUseruns before the tool,PostToolUseafter. - Make scripts executable. Run
chmod +x ./my-hook.sh. Use absolute paths or$CLAUDE_PROJECT_DIRto avoid "command not found" errors, and installjqif you see "jq: command not found". - Run it by hand. Pipe sample JSON in and read the exit code:
printf '%s' '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh
printf 'exit code: %s\n' "$?"
- Keep stdout clean. If your hook prints JSON, anything printed before it, often a line your shell profile prints, makes Claude Code read the output as plain text and ignore the JSON.
- Read the debug log. Start with
claude --debug-file /tmp/claude.logand runtail -f /tmp/claude.login another terminal, or run/debugmid-session to turn logging on. - Check
disableAllHooks."disableAllHooks": truein a settings file turns hooks off, and a project file can override yours.
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.