60 60. Hooks: Running Your Own Commands Automatically
Claude usually follows your instructions, but “usually” isn’t good enough for some rules. A hook attaches your own command to a moment in Claude Code’s work, such as “right after Claude edits a file”, so that it runs every single time. This page shows how to set hooks up and how to use them safely.
60.1 What a hook is
As of Oct 6, 2026 · Official docs: Automate actions with hooks
A hook is a command (usually a shell command) that Claude Code runs automatically at a specific point in a session’s lifecycle: when the session starts, when you submit a prompt, before or after Claude uses a tool, when Claude finishes responding, and so on. Each of these points is called a hook event. Think of a rule in your email program: “whenever a message from my manager arrives, flag it.” You set it up once, and it fires every time.
Why not just put the rule in CLAUDE.md? CLAUDE.md and skills hold instructions that Claude usually follows. A hook is run by Claude Code itself, not by the AI model, so it is deterministic: it always happens. In the docs’ words, “never edit .env” in CLAUDE.md is a request; a hook that blocks the edit is enforcement.
Typical uses: a desktop notification when Claude needs you, formatting code after every edit, blocking dangerous commands or edits to protected files, logging what Claude does, and adding information to Claude’s context when a session starts.
60.2 Hook events
As of Oct 6, 2026 · Official docs: Hooks reference
Claude Code has more than 30 hook events. These are the most useful ones. (“Tool” means one of Claude’s actions, such as Read, Edit, Write, or Bash; see How Claude Code Works.)
| Event | When it fires | Can it block? |
|---|---|---|
SessionStart |
A session starts or resumes | No |
UserPromptSubmit |
You submit a prompt, before Claude sees it | Yes (the prompt is rejected) |
PreToolUse |
Before a tool call runs | Yes (the call is cancelled) |
PermissionRequest |
Claude Code is about to ask you for permission | Yes, with JSON output |
PostToolUse |
After a tool call succeeds | No (the tool already ran) |
Notification |
Claude Code needs your attention (permission, idle, …) | No |
Stop |
Claude finishes responding | Yes (Claude keeps working) |
SubagentStop |
A subagent finishes | Yes (it keeps working) |
SessionEnd |
The session ends | No |
The reference lists the rest (compaction, configuration changes, model switches, files changing on disk, and more).
60.3 How a hook is configured
As of Oct 6, 2026 · Official docs: Hooks reference: Configuration
Hooks live in a "hooks" section of a JSON settings file, in three levels:
"hooks"
└── event name, e.g. "PostToolUse" WHEN should it run?
└── matcher group: "matcher": "Edit|Write" WHICH occurrences?
└── "hooks": [ handler, ... ] WHAT should run?
This complete example appends a line to a log file in your home folder after every Edit or Write:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "echo 'Claude changed a file' >> ~/claude-edits.log"
}
]
}
]
}
}- Matcher. For tool events it is the tool’s name.
Edit|WritemeansEditorWrite;"","*", or no matcher means every occurrence. Matchers are case-sensitive (editmatches nothing). Some events, such asUserPromptSubmitandStop, ignore matchers. - Handler. What to run. Most hooks are
"type": "command", a shell command. Other types call a web address (http), a tool on an MCP server (mcp_tool), a Claude model for a yes/no decision (prompt), or a checking subagent (agent, experimental).
Where to put it. The file decides the hook’s scope (see The .claude Folders and Settings Files):
| File | Applies to | Shared with your team? |
|---|---|---|
~/.claude/settings.json |
All your projects | No |
.claude/settings.json in the project |
This project | Yes (commit it) |
.claude/settings.local.json in the project |
This project, just you | No (git-ignored) |
Plugins, skill and subagent files, and an organization’s managed settings can add hooks too; hooks from all sources are combined. If a file already has a "hooks" section, add new events inside it (JSON allows no comments or trailing commas). Or simply ask Claude: “add a hook to my user settings that …”.
What a hook receives. Claude Code sends the hook a JSON description of the event on its standard input (stdin). For a Bash command (shortened):
{
"session_id": "abc123",
"cwd": "/Users/maria/myproject",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test"
}
}Scripts usually read it with jq, a small command-line JSON tool (Mac: brew install jq if you use Homebrew; otherwise see jq’s download page). On Windows, Claude Code runs command hooks in Git Bash (or PowerShell if Git Bash isn’t installed), so the Bash examples on this page work for Sam too (example 2 needs one small Windows change).
60.4 Three useful hooks
As of Oct 6, 2026 · Official docs: Automate actions with hooks: What you can automate
60.4.1 1. Get notified when Claude needs you
Add this to ~/.claude/settings.json (create the file if needed):
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
}
]
}
]
}
}No notification? Run osascript -e 'display notification "test"' once, then in System Settings > Notifications > Script Editor turn on Allow Notifications.
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "powershell.exe -Command \"[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('Claude Code needs your attention', 'Claude Code')\""
}
]
}
]
}
}This asks PowerShell to open a dialog box, which may appear behind your terminal window.
The empty matcher fires on every notification type, such as permission_prompt (a permission prompt has waited about six seconds) and idle_prompt (Claude finished about 60 seconds ago and you haven’t typed). To test, check /hooks, press Shift+Tab until the status bar shows Manual mode (⏸ manual mode on), ask for something that needs permission, and switch to another window.
60.4.2 2. Format code after every edit
In a JavaScript project with Node.js (and jq) installed, this project hook in .claude/settings.json runs the Prettier code formatter on every file Claude edits or writes:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}jq pulls the edited file’s path out of the JSON input, and xargs hands it to Prettier, which rewrites the file in a consistent style. A successful hook shows nothing in the conversation; you see the result in the file.
On Windows, Claude Code sends the path with backslashes (C:\Users\sam\...), and xargs treats backslashes as special and drops them, so Prettier can’t find the file. In his settings, Sam can use this handler (the innermost { ... } object) instead, which passes the path through unchanged:
{
"type": "command",
"command": "npx prettier --write \"$(jq -r '.tool_input.file_path')\""
}60.4.3 3. Block edits to .env files
Save the official example script as .claude/hooks/protect-files.sh in your project:
#!/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 0If the file path contains a protected pattern, the script writes a reason to stderr (the error-message channel; >&2 sends echo’s text there) and exits with code 2, which means “block”. On a Mac, make it executable (the docs list this step for macOS and Linux):
chmod +x .claude/hooks/protect-files.shRegister it in .claude/settings.json as a PreToolUse hook, so it runs before every Edit or Write. $CLAUDE_PROJECT_DIR is an environment variable holding your project’s root folder:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
}
]
}
]
}
}Now, when Claude tries to edit .env, Claude Code cancels the edit and passes the Blocked: ... message to Claude, which can change its approach. Test the script without Claude by piping in fake event JSON:
echo '{"tool_name":"Edit","tool_input":{"file_path":".env"}}' | .claude/hooks/protect-files.sh
echo $?Output:
Blocked: .env matches protected pattern '.env'
2
With the matcher Edit|Write, the hook only sees those two tools. Claude can also change files through the Bash tool (for example echo "X=1" >> .env). For complete coverage the docs suggest also matching Bash|PowerShell (on Windows, Claude often runs shell commands with its PowerShell tool) and checking git status --porcelain, or adding a Stop hook that scans the project once per turn. Permission deny rules (Permissions, Modes, and Safety) are another layer.
60.5 How a hook blocks an action: exit codes
As of Oct 6, 2026 · Official docs: Hooks reference: Exit code output
Every program ends with an exit code, a number that reports how it went. Claude Code reads your hook’s exit code:
| Exit code | Meaning | What happens |
|---|---|---|
0 |
No objection | The action continues. For PreToolUse this does not approve the call: the normal permission check still applies. For SessionStart and UserPromptSubmit, text the hook prints is added to Claude’s context. |
2 |
Block | On events that can block, the action is stopped, and your stderr text is the reason (for PreToolUse, Claude reads it as feedback). |
Anything else (e.g. 1) |
Non-blocking error | The action continues; the transcript shows a hook error notice. |
Only exit 2 blocks. A guard can also fail quietly: a mistyped script path ends in a non-blocking error, and a script that can’t find a tool it needs (such as jq) may simply reach exit 0. Either way the action goes ahead. Test guard scripts by hand, as shown above.
Instead of exiting 2, a hook can exit 0 and print a JSON object. For example, a PreToolUse hook can deny a call and explain why:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Use rg instead of grep for better performance"
}
}permissionDecision can also be "allow" (skip the permission prompt) or "ask" (show it as usual); "allow" never overrides deny or ask rules in your settings. Other events use other fields: Stop hooks use "decision": "block" with a "reason", and PermissionRequest hooks return a decision object. Use one approach per hook: exit code 2, or exit 0 with JSON.
60.6 Viewing, testing, and turning hooks off
As of Oct 6, 2026 · Official docs: Hooks reference: The /hooks menu · Limitations and troubleshooting
- See your hooks:
/hooksopens a read-only list of hooks by event, labeled with where each comes from (user, project, or local settings, a plugin, …). To change a hook, edit the settings file; Claude Code normally picks up the change automatically. - Debug: press
Ctrl+Ofor the transcript view, or start withclaude --debug(or run/debugmid-session) to log which hooks matched, their exit codes, and their output. - Turn hooks off: delete a hook’s entry to remove it. To switch off all hooks without deleting them, add
"disableAllHooks": trueto a settings file (this also turns off a custom status line); there is no switch for a single hook.claude --safe-modestarts one session with hooks and your other customizations disabled.
60.7 Hooks and safety
As of Oct 6, 2026 · Official docs: Hooks reference: Security considerations
“Command hooks execute shell commands with your full user permissions.” A hook can read, change, or delete any file your account can. Read every hook before you add it, especially one written by someone else.
- Cloned projects. A repository’s
.claude/settings.jsoncan contain hooks. In an interactive session, Claude Code holds hooks back until you accept the workspace trust prompt for the folder. Butclaude -p(non-interactive mode) treats the folder as trusted, so the hooks run right away. In a repository you didn’t write, read its.claude/settings first, or runclaude -p "..." --settings '{"disableAllHooks": true}'. - Permission modes.
PreToolUsehooks run in every permission mode. A hook that denies a call blocks it even inbypassPermissionsmode, but a hook that says “allow” can’t override deny rules in your settings. - Plugins and mods. Plugins can bundle hooks, so installing one can add hooks. A mod is a newer kind of plugin whose “hooks” are JavaScript functions running inside Claude Code; the docs call the kind on this page settings hooks to tell them apart.
/goal
A prompt hook sends your question plus the event data to a Claude model, which answers {"ok": true} or {"ok": false, "reason": "..."}. On Stop, "ok": false makes Claude keep working, with the reason as its next instruction:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Check if all tasks are complete. If not, respond with {\"ok\": false, \"reason\": \"what remains to be done\"}."
}
]
}
]
}
}An agent hook (experimental) starts a subagent that can read files and run commands, for example to verify that tests pass before Claude stops. The built-in /goal command is a shortcut for a session-only prompt-based Stop hook (see Working Effectively with Claude Code). To prevent endless loops, Claude Code overrides a Stop hook after it blocks eight times in a row.
60.8 Key takeaways
- A hook runs your command at a lifecycle event, every time; CLAUDE.md instructions are only requests.
- Configuration = event → matcher → handler, in a settings file whose location sets the scope.
- In
PreToolUse,exit 2blocks; other non-zero exit codes don’t. - Hooks run with your permissions: review them, and check
/hooks.
Practice Questions
Easy
Question 1 · Short answer
In one or two sentences, what is a hook in Claude Code?
Question 2 · Multiple choice
Which hook event runs before Claude uses a tool and can stop the tool call?
PostToolUsePreToolUseNotificationSessionStart
Question 3 · True or false
If your CLAUDE.md file says “Never edit the .env file”, Claude Code guarantees that .env will never be edited.
Question 4 · Matching
Match each hook event with when it fires.
| Event | When it fires | |
|---|---|---|
1. SessionStart |
A. Claude finishes responding | |
2. UserPromptSubmit |
B. After a tool call succeeds | |
3. PostToolUse |
C. A session begins or resumes | |
4. Notification |
D. You submit a prompt, before Claude processes it | |
5. Stop |
E. Claude Code needs your attention (permission, idle) |
Question 5 · Fill in the blank
To see a read-only list of all the hooks that are currently configured, type ________ at the Claude Code prompt.
Question 6 · Multiple choice
Maria wants a notification hook that works in all of her projects, without sharing it with anyone. Which file should she put it in?
~/.claude/settings.json.claude/settings.jsonin each project.claude/settings.local.jsonin each projectCLAUDE.md
Medium
Question 7 · Read the config
Explain in plain English what this configuration does, and when.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}Question 8 · Predict the outcome
Sam writes a PreToolUse guard script that prints Not allowed! to stderr and ends with exit 1 when Claude tries to run git push. What happens when Claude runs git push? How should Sam fix it?
Question 9 · Write the config
Write a complete settings file that, every time Claude finishes responding, appends the line Claude finished to the file ~/claude-stops.log.
Question 10 · Find the bug
This hook is supposed to log every file edit, but it never runs. Find the bug.
{
"hooks": {
"PostToolUse": [
{
"matcher": "edit|write",
"hooks": [
{ "type": "command", "command": "echo 'edited' >> ~/edits.log" }
]
}
]
}
}Question 11 · Matching
Match each PreToolUse matcher with what it matches.
| Matcher | Matches | |
|---|---|---|
1. "Bash" |
A. Every tool call | |
2. "Edit|Write" |
B. Only shell commands | |
3. "" |
C. Every tool from an MCP server named github |
|
4. "mcp__github__.*" |
D. The Edit tool or the Write tool |
Question 12 · Scenario
Your team wants everyone who works on the club website repository to get the same auto-format hook. You also want a personal hook that logs commands only on your machine. Where does each hook go?
Question 13 · Multiple choice
The protect-files.sh script on this page blocks an edit with echo "Blocked: ..." >&2 followed by exit 2. What happens to the Blocked: ... text?
- It is thrown away
- It is shown to Claude as the reason the edit was blocked
- It is saved into the
.envfile - It is emailed to the project owner
Hard
Question 14 · Predict the output
With the protect-files.sh hook from this page installed, which of these edits are blocked? Explain any surprise.
.env.example.gitignoresrc/my.environment.tsREADME.md
Question 15 · Long answer
Why can’t a PostToolUse hook stop Claude from editing a protected file? Which event should you use instead, and why?
Question 16 · Short answer
A PreToolUse hook exits with code 0 for a Bash call. Does that mean the command is approved and will run without asking you? Explain.
Question 17 · Find the bug
Maria installed protect-files.sh as a PreToolUse hook, but Claude still edits .env freely and no error appears. When she tests the script by hand, she sees this:
$ echo '{"tool_name":"Edit","tool_input":{"file_path":".env"}}' | .claude/hooks/protect-files.sh
.claude/hooks/protect-files.sh: line 5: jq: command not found
$ echo $?
0
Explain what is going on and how to fix it.
Question 18 · Scenario
Sam clones a classmate’s repository and wants to run claude -p "summarize this project" in it. Why could that be risky with respect to hooks, and what are two ways to reduce the risk?
Question 19 · Compare
The protect-files.sh hook uses the matcher Edit|Write. Explain how Claude could still change .env without the hook noticing, and describe one way to close that gap.
Challenge
Question 20 · Hands-on
Set up the desktop notification hook for your operating system in ~/.claude/settings.json. Confirm it appears in /hooks, trigger it, and then turn all hooks off with "disableAllHooks": true and confirm that the notification no longer appears. Write down what you did and what you saw.
Question 21 · Hands-on
In a practice project (for example the Git practice project Download the Git practice project (zip)), add a .env file containing API_KEY=test, install the protect-files.sh hook from this page, and test the script by hand. Then start Claude Code in the project and ask it to add a line to .env. What happens, and what does Claude say?
Question 22 · Hands-on
Ask Claude Code to write a hook for you: “Add a project hook that appends every Bash command you run to .claude/command-log.txt.” Before approving the change, read the JSON Claude proposes and identify the event, the matcher, and the handler. Then test it and look at the log file.