Before you start
You need Claude Code from lesson 1 and a settings file you can edit. The first hook sends a desktop notification, so it suits a machine with a desktop. The second hook is a Bash script that calls jq, which Anthropic's hooks guide installs with brew install jq on macOS or apt-get install jq on Debian and Ubuntu.
One rule applies to every hook you write. Anthropic's reference warns about what they can reach.
Command hooks execute shell commands with your full user permissions. They can modify, delete, or access any files your user account can access. Review and test all hook commands before adding them to your configuration.
Set up your first hook: a notification when Claude needs you
Anthropic's own walkthrough starts with a notification, so you do not have to watch the terminal while Claude waits. The block below is the macOS version. It uses the Notification event, which fires when Claude is waiting for your input or permission.
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
}
]
}
]
}
}- Open
~/.claude/settings.json, creating it if it does not exist, and add the block above. On Linux, usenotify-send 'Claude Code' 'Claude Code needs your attention'as thecommand. On Windows, copy the PowerShell command from the Windows tab of the hooks guide. - If the file already has a
hookskey, addNotificationnext to the event names that are there. Each event is a key inside the onehooksobject, so do not replace the whole object. - Save, then type
/hooksat the Claude Code prompt. Your hook shows up in the list underNotification. - Press
Escto go back to the prompt. PressShift+Tabuntil the status bar shows⏸ manual mode on, ask Claude to do something that needs permission, then switch to another window. A desktop notification should arrive. Manual mode matters here because in auto mode a classifier reviews actions instead of asking you (lesson 2).
Events, matchers and where a hook lives
A hook has three layers, as the reference describes. First an event, like PreToolUse or Stop. Then a matcher group that filters when it fires, like "only for the Bash tool". Then the handler, which is the shell command. Your first hook used the empty matcher "", which fires on every notification type. Setting it to permission_prompt would fire it only when Claude needs approval, after the prompt has waited about six seconds.
For tool events the matcher is a tool name. Bash matches the Bash tool, and Edit|Write matches either of two tools. The guide says matchers are case-sensitive and must match the tool name exactly.
Where you save the block decides who gets the hook.
33
hook events, from SessionStart to SessionEnd
Source: Claude Code docs: Hooks reference (read November 5, 2026)
5
handler types: command, http, mcp_tool, prompt and agent
Source: Claude Code docs: Hooks reference (read November 5, 2026)
600 s
is the default timeout for command, http and mcp_tool hooks on most events
Source: Claude Code docs: Hooks reference (read November 5, 2026)
1.5 s
is all a SessionEnd hook gets by default when you exit or run /clear
Source: Claude Code docs: Hooks reference (read November 5, 2026)
~/.claude/settings.json: all your projects, and not shared with anyone..claude/settings.json: one project, and it can be committed so the team gets it too..claude/settings.local.json: one project, and not committed.- A plugin's
hooks/hooks.json, or a skill's frontmatter: the hook travels with that plugin or skill.
| Event | When it fires | What the matcher filters |
|---|---|---|
SessionStart | When a session begins or resumes | How it started, such as startup or resume |
UserPromptSubmit | When a prompt is submitted, before Claude processes it | Nothing, it always fires |
PreToolUse | Before a tool call executes, and it can block the call | The tool name |
PostToolUse | After a tool call succeeds | The tool name |
Notification | When Claude Code sends a notification | The notification type |
Stop | When Claude finishes responding | Nothing, it always fires |
PreCompact | Before context compaction | manual or auto |
SessionEnd | When a session terminates | Why it ended |
A hook that says no: protect files from edits
A notification only reports. A PreToolUse hook can stop the action. This script, from the guide, reads the file path Claude is about to edit and refuses .env, package-lock.json and anything under .git/. The message it prints to stderr goes back to Claude, so Claude can adjust.
It parses JSON with jq and exits 2 to block. Save it as .claude/hooks/protect-files.sh.
#!/bin/bash
# protect-files.sh
INPUT=$(cat)
FILE_PATH=$(echo "$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
echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
exit 2
fi
done
exit 0- Make the script executable on macOS and Linux with
chmod +x .claude/hooks/protect-files.sh. Claude Code cannot run a hook script that is not executable. - Register it in
.claude/settings.jsonas aPreToolUsehook with the matcherEdit|Write. Thecommandis"$CLAUDE_PROJECT_DIR"/.claude/hooks/protect-files.sh, written with escaped quotes inside the JSON string. - Ask Claude to add a comment to your
.envfile. Claude Code blocks the edit before it runs and passes theBlocked:message to Claude.
How a hook answers: exit codes and input
Claude Code sends the event as JSON on the hook's standard input. Every event carries fields like session_id and cwd, and each type adds its own. A PreToolUse hook for Bash sees the tool name and the command, as in the example below. That is why the script above reads .tool_input.file_path for an edit.
The hook answers with its exit code. Exit 0 from a PreToolUse hook is not an approval, because the tool call still goes through the normal permission flow. For a hook that must decide with more detail, Claude Code also reads a JSON object from stdout when you exit 0. Pick one approach per hook, exit 2 or JSON, and do not mix them.
{
"session_id": "abc123",
"cwd": "/Users/sarah/myproject",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test"
}
}| Exit code | What Claude Code does | Where your message goes |
|---|---|---|
| 0 | Success, and the action goes ahead | Plain-text stdout reaches Claude only on UserPromptSubmit, UserPromptExpansion, SessionStart and PostModelSwitch |
| 2 | Blocking error, so on events that can block, the action stops | Your stderr becomes the reason, and Claude gets it as feedback on most events |
| Anything else | Non-blocking error, so the action goes ahead | A hook error notice shows the first line of stderr after "Failed with non-blocking status code:" |
How Claude Code's creator uses a format hook and notifications
Boris Cherny created Claude Code at Anthropic. His January 2026 setup thread on X says his team formats Claude's code with a PostToolUse hook. Claude usually writes well-formatted code, and the hook fixes the rest before a CI check fails on it. The auto-format hook in "Try it yourself" below is the same idea: PostToolUse, the matcher Edit|Write and Prettier.
The first tip in that thread is about waiting. He runs five Claude sessions at once in numbered terminal tabs and relies on system notifications to learn when one needs input. Your first hook in this lesson gives you that signal in any terminal, and the permission_prompt matcher narrows it to approval requests.
Claude usually generates well-formatted code out of the box, and the hook handles the last 10% to avoid formatting errors in CI later.
Screenshots of X's own embeds of the posts, taken on 5 November 2026, with the time in UTC. Select a screenshot to open the post on X.
Common mistakes
- A matcher that does not match. Matchers are case-sensitive and compare against the tool name, not the words in your prompt. Check the spelling, then check
/hooksto see the hook under the event you meant. - Replacing the
hooksobject instead of adding to it. Each event name is a sibling key inside the onehooksobject. - Invalid JSON in the settings file. Trailing commas and comments are not allowed, and a broken file leaves
/hookswith nothing configured. Edits are normally picked up on their own. If a few seconds pass and nothing appears, restart the session. - A script that is not executable, or one that cannot be found. Run
chmod +xon it and point to it with$CLAUDE_PROJECT_DIR. If the transcript saysjq: command not found, installjq. - Expecting a successful hook to say something. It shows nothing, so check its effect, like a reformatted file, or press
Ctrl+Oto open the transcript view. - Expecting
PostToolUseto undo anything. The tool has already run. A formatter hook with the matcherEdit|Writealso misses files that a Bash command rewrites.
Try it yourself
- Change the notification hook's matcher to
permission_promptand repeat the test. Then set it back to""and compare when each one fires. - Test the protect script by hand before you trust it:
echo '{"tool_input":{"file_path":".env"}}' | .claude/hooks/protect-files.sh, thenecho $?. You should see the Blocked line and a 2. - Open
/hooksand select All events at the end of the list, to browse every event, including the ones with no hook. - Add the guide's auto-format hook to
.claude/settings.json. It is aPostToolUsehook with the matcherEdit|Writethat runs Prettier on the edited file, and it needsjqand Prettier. Ask Claude to add single-quoted strings to a JavaScript file, then open the file.
Next: Agent Skills
That is the Claude Code course: install, plan mode, slash commands and hooks. The next course, Agent Skills, goes deeper on the SKILL.md format behind the commands you wrote in lesson 3.
Hooks also travel inside plugins, which our plugins guide covers. A project's standing instructions still belong in a CLAUDE.md file, because a hook enforces and a CLAUDE.md file explains.
Check yourself
Your
PreToolUsehook script must block an edit. Which exit code does it use?Show answer
C. Exit 2 Exit 2 blocks the tool call and sends your stderr message to Claude, while exit 1 is a non-blocking error and the action goes ahead.
Where do you save a hook so it applies to one project and your team gets it when you commit?
Show answer
A.
.claude/settings.jsonThat file belongs to one project and can be committed, while~/.claude/settings.jsoncovers all your projects and is not shared.Which matcher makes a
PreToolUsehook fire for either the Edit tool or the Write tool?Show answer
D.
Edit|WriteEdit|Writematches either tool, and matchers are case-sensitive and must match the tool name exactly.Your settings file already has a
hookskey and you want to add a second event. What do you do?Show answer
B. Add the new event name beside the others inside the one
hooksobject Each event is a key inside the onehooksobject, so you add to it and never replace it.
Frequently asked questions
What are Claude Code hooks used for?
They run shell commands at points in a session, for example to format files after edits, block commands before they run, send a notification when Claude needs input, or add context at session start. Anthropic describes the gain as deterministic control: certain actions always happen, rather than relying on the model to choose to run them.
How do I see which hooks are active?
Type /hooks. It opens a read-only browser of your configured hooks, each labeled with where it comes from, such as user settings, project settings or a plugin. Select a hook to see the full command and the file that defines it.
Can a hook stop Claude from doing something?
Yes. A PreToolUse hook that exits 2, or returns a deny decision, cancels the tool call. That block holds in every permission mode, including bypassPermissions, because PreToolUse hooks fire before the permission-mode check.
How do I turn hooks off?
Set "disableAllHooks": true in your settings file, or pass --settings '{"disableAllHooks": true}' to turn them off for one run. You cannot disable one hook and keep it in the configuration. Hooks set in managed settings keep running unless that setting is also on at the managed level.
Are hooks from the internet safe to copy?
Only after you have read them. Whatever the command does, it does as you, on any file your account can reach. In an interactive session, Claude Code also holds back hooks from every settings file until you accept the workspace trust dialog for the folder.
Do I need to restart Claude Code after editing a hook?
Usually not. When you edit a settings file while Claude Code runs, the file watcher normally picks up hook changes on its own. If /hooks still does not show your change after a few seconds, restart the session to force a reload.
Can a skill or a plugin carry its own hooks?
Yes. A plugin ships hooks in hooks/hooks.json, and a skill can declare them in its frontmatter, where they run for the rest of the session once the skill is invoked. A subagent's frontmatter can too, and those hooks run while that subagent is running.
Sources
- Claude Code docs: Automate actions with hooks https://code.claude.com/docs/en/hooks-guide
- Claude Code docs: Hooks reference https://code.claude.com/docs/en/hooks
- Boris Cherny on X: tip 9, a PostToolUse format hook, 2 January 2026 https://x.com/bcherny/status/2007179852047335529
- Boris Cherny on X: tip 1, five sessions and notifications, 2 January 2026 https://x.com/bcherny/status/2007179833990885678


