This procedure adds a hook to Claude Code: a user-defined handler, most often a shell command, that Claude Code runs at a specific point in its lifecycle, so that an action always happens rather than depending on the model choosing to run it (see Agent Hooks). Anthropic’s guide walks it through with a desktop notification that fires whenever Claude is waiting for the user’s input.
Prerequisites
- Claude Code, and write access to one of the settings files that can hold hooks:
~/.claude/settings.jsonfor all of your projects,.claude/settings.jsonfor a project and shareable through the repository, or.claude/settings.local.jsonfor a project but not shared. Hooks can also come from managed policy settings, a plugin’shooks/hooks.json, or a skill’s or subagent’s frontmatter. - For the guide’s command examples that parse the hook’s JSON input,
jq. - A hook script called from the command must be executable on macOS and Linux (
chmod +x).
Steps
- Open the settings file for the scope you want and add a
hooksblock. Each event name is a key inside the singlehooksobject, holding a list of groups with an optionalmatcherand the handlers to run; for the notification example that is aNotificationevent with an empty matcher and acommandhandler that calls the platform’s notification command (osascripton macOS,notify-sendon Linux, a PowerShell dialog on Windows). If the file already has ahookskey, add the new event as a sibling rather than replacing the object. Claude can also be asked to write the hook. - Narrow when it fires, if needed. Without a matcher a hook fires on every occurrence of its event;
a matcher filters on a field that depends on the event — the tool name for tool events (for example
Edit|Write), the notification type forNotification, how the session started forSessionStart. On tool events aniffield using permission-rule syntax, such asBash(git *), filters by tool name and arguments together. - Type
/hooksto open the hooks browser, select the event, and confirm the new hook appears with its matcher, type, source file and command. The menu is read-only; changes are made in the settings JSON. - Test it by triggering the event. For the notification example, switch Claude Code to manual mode with
Shift+Tab, ask for something that needs permission, switch away from the terminal, and wait for the notification.
Notes
- A command hook receives the event’s data as JSON on stdin and answers through its exit code and
output. Exit 0 reports no objection (for
PreToolUseit does not approve the call; the normal permission flow still applies). Exit 2 blocks the action, with stderr as the reason. Alternatively, a hook can exit 0 and print a JSON object for structured control, such as aPreToolUsepermissionDecisionofallow,denyorasknested insidehookSpecificOutput. - Besides
command, a hook’stypecan behttp(POST the event data to a URL),mcp_tool,prompt(a single-turn evaluation by a Claude model, Haiku by default) oragent(a subagent that can use tools to verify a condition; described as experimental). - The guide’s worked examples include auto-formatting files after edits with a
PostToolUsehook, blocking edits to protected files with aPreToolUsescript that exits 2, re-injecting critical context after Compaction with aSessionStarthook on thecompactmatcher, logging configuration changes, reloading environment variables when the working directory changes, and auto-approving a specific permission prompt with aPermissionRequesthook — which the guide warns to keep narrowly matched. - When several hooks match one event they all run, in parallel, before results are combined; for
PreToolUsethe most restrictive decision wins. APreToolUsedeny blocks the tool in every Permission Modes setting, includingbypassPermissions, but a hook returningallowcannot override deny rules from settings — hooks can tighten restrictions but not loosen them. - If a hook does not fire, check that it appears under the right event in
/hooksand that the matcher matches the tool name exactly (matchers are case-sensitive). If JSON output has no effect, look for shell-profileecholines being prepended to stdout, or fields placed at the wrong level. AStophook that keeps blocking is overridden after eight consecutive blocks without progress, so it should exit early when thestop_hook_activeinput field is true.