Claude Code hooks: run your own scripts on every edit, command and prompt
A hook is a command Claude Code runs itself at a set moment, such as before a tool call, after a file edit or when Claude finishes. You add hooks in a settings file under "hooks", by event and matcher. Unlike an instruction in CLAUDE.md, a hook runs every time, and a PreToolUse hook that exits with code 2 blocks the action.
Extend
Take this page into your AI
Open in ChatGPT (new tab)Open in Claude (new tab)Instructions in CLAUDE.md are advice: Claude reads them and usually follows them. A hook is not advice. It is a command that Claude Code itself runs at a fixed moment, such as just before Claude runs a tool, just after it edits a file, or when it finishes answering. It runs every time, whatever Claude decides.
Use a hook for anything that must always happen: format each file after an edit, refuse writes to a folder, tell you when Claude is waiting, or put fresh facts into the conversation when a session starts.
How a hook is set up
Hooks go in a settings file under "hooks", in 3 levels: the event (when), a matcher (which tool or case) and the handlers (what runs). This one runs a formatter after every edit:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/format.sh" }
]
}
]
}
}
And the script, .claude/hooks/format.sh, made executable with chmod +x:
#!/bin/bash
# Claude Code sends the tool call as JSON on stdin
file=$(jq -r '.tool_input.file_path // empty')
case "$file" in
*.ts|*.tsx|*.js|*.json|*.css) npx prettier --write "$file" ;;
esac
exit 0
Put it in .claude/settings.json to share it with the team, .claude/settings.local.json for yourself in this project, or ~/.claude/settings.json for every project. Hooks from all the files run together. $CLAUDE_PROJECT_DIR is the project's root, so the script is found wherever Claude happens to be working.
Type /hooks in a session to see every hook that is loaded and where it came from. It only shows them; you change hooks in the files, and Claude Code picks the change up without a restart.
Matchers
The matcher says which cases fire the hook. For tool events it is the tool's name:
"*",""or no matcher: every case.- Plain names such as
BashorEdit|Write: exactly those names. - Anything with other characters is a regular expression that may match anywhere in the name, so
Edit.*also catchesNotebookEdit. Write^Edit$to be exact, andmcp__github__.*for every tool of one MCP server.
Other events match other things: SessionStart matches how the session began (startup, resume, clear, compact), Notification the kind of notice. Some, such as Stop and UserPromptSubmit, take no matcher and always fire. The if field narrows a handler further with one permission rule, such as "if": "Bash(git push *)", so the script starts only for those commands.
The events you will use most
| Event | Fires | Can block |
|---|---|---|
SessionStart | When a session starts, resumes or is compacted | No |
UserPromptSubmit | When you send a prompt, before Claude sees it | Yes |
PreToolUse | Before a tool runs | Yes |
PostToolUse | After a tool succeeds | No, it already ran |
Notification | When Claude Code needs your attention | No |
Stop | When Claude finishes answering | Yes: Claude keeps going |
SubagentStop | When a subagent finishes | Yes |
PreCompact | Before the conversation is compacted | Yes |
There are over 30 events in all, for permissions, tasks, worktrees, files changing and more. Search them in the list at the end of this page.
What a hook receives and returns
Claude Code passes the details as JSON on the hook's standard input: the session id, the working folder, the permission mode, the event's name and, for tool events, tool_name and tool_input, such as the file path or the command.
The hook answers with its exit code:
- 0: carry on. For
SessionStartandUserPromptSubmit, whatever it prints is added to the conversation for Claude to read. - 2: stop. What it writes to stderr becomes the reason, which Claude sees. A
PreToolUsehook cancels the tool call this way; aStophook makes Claude keep working. - Anything else: an error that is reported and otherwise ignored, and the action goes ahead.
For finer control a hook can print a JSON object instead, for example to deny a tool call with a reason, or to ask you rather than decide.
Example: keep Claude out of a folder
Applied database migrations must never be edited. This PreToolUse hook refuses any edit to db/migrations/:
#!/bin/bash
# .claude/hooks/protect-migrations.sh
file=$(jq -r '.tool_input.file_path // empty')
if [[ "$file" == *"/db/migrations/"* ]]; then
echo "Applied migrations are read-only. Write a new migration instead." >&2
exit 2
fi
exit 0
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-migrations.sh" }
]
}
]
}
}
A deny from PreToolUse holds in every permission mode, even when permissions are skipped. The other way does not work: a hook that allows something cannot override a deny rule. A hook checks only the tool it matches, though, so a determined Bash command could still write the file. For a hard limit, add a deny rule or the sandbox as well.
Example: a notification when Claude is waiting
In ~/.claude/settings.json, so it works in every project (macOS):
{
"hooks": {
"Notification": [
{
"hooks": [
{ "type": "command", "command": "osascript -e 'display notification \"Waiting for you\" with title \"Claude Code\"'" }
]
}
]
}
}
On Linux, notify-send "Claude Code" "Waiting for you" does the same.
Example: facts back after compaction
A summary can drop details. A SessionStart hook with the compact matcher runs right after each CompactionCompactionReplacing the older part of a long conversation with a summary, so an agent can keep working when its context fills up., and what it prints goes back into the conversation:
{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{ "type": "command", "command": "echo 'Use pnpm, not npm.'; git status --short" }
]
}
]
}
}
Things that trip people up
- Several hooks on one event run at the same time, not in order. If any one blocks, the action is blocked, but the others have still run.
Stopfires every time Claude finishes answering, not only when the whole task is done. A Stop hook that blocks keeps Claude working; after 8 blocks in a row Claude Code overrides it. Check thestop_hook_activefield in the input and let Claude stop if it is alreadytrue.- Timeouts: a command hook may take up to 10 minutes by default (30 seconds on
UserPromptSubmit). Set"timeout"in seconds for anything slow. - Not firing? Check
/hooks, that the matcher's case matches the tool name, and that the script is executable. Test it by hand:echo '{"tool_input":{"file_path":"db/migrations/1.sql"}}' | .claude/hooks/protect-migrations.sh; echo $?. - Besides commands, a hook can call a web address (
"type": "http"), an MCP tool, or have a Claude model judge the case and answer yes or no ("type": "prompt").
Hooks run your code: check them first
A command hook runs with your full user permissions, not inside Claude's permission rules. A repository's .claude/settings.json can define hooks, so cloning a project and opening Claude Code in it can run its code. In an interactive session, Claude Code holds back a folder's hooks until you trust it. Read the file before you say yes.
Scripts that run claude -p in a repository you did not write should add --bare, which skips hooks and other settings found on disk. In your own hook scripts, put variables in quotes ("$file"), use full paths, and never pass text from the tool call straight to a shell.
"disableAllHooks": true in your settings turns off every hook except those an organization manages.
33 hook events
SessionStartSessionRuns when a session starts, resumes, is cleared or is compacted. What the hook prints is added to the conversation, so it suits loading fresh facts such as the Git status.
"matcher": "startup|compact"
SessionEndSessionRuns when a session ends, for cleanup or logging. It cannot stop the session, and its hooks share a short time budget.
"matcher": "logout"
SetupSessionRuns once for preparation when you start Claude Code with --init-only, or with --init or --maintenance in -p mode, such as in CI.
"matcher": "init"
ConfigChangeSessionRuns when a settings file or skill changes during a session. Exit 2 stops the change taking effect, except for managed settings.
"matcher": "project_settings"
PreModelSwitchSessionRuns before a model switch you asked for is applied. Exit 2 blocks the switch. The matcher is the model being switched to.
"matcher": ".*opus.*"
PostModelSwitchSessionRuns after the session's model has changed, including changes Claude Code makes itself. What it prints is added to the conversation.
UserPromptSubmitPromptRuns when you send a prompt, before Claude sees it. It can add context, or exit 2 so the prompt never reaches Claude. Takes no matcher.
UserPromptExpansionPromptRuns when a command you typed turns into a prompt, before it reaches Claude. Exit 2 blocks it. The matcher is the command's name.
"matcher": "deploy"
NotificationPromptRuns when Claude Code wants your attention, such as a permission prompt or Claude waiting for input. The usual place for a desktop notification.
"matcher": "permission_prompt|idle_prompt"
MessageDisplayPromptRuns while Claude's answer text is shown. Takes no matcher.
StopPromptRuns each time Claude finishes answering, not only at the end of a task. Exit 2 makes Claude keep working; check stop_hook_active so it can stop in the end.
StopFailurePromptRuns when a turn ends because of an API error, such as a rate limit. Its output and exit code are ignored, so use it for logging or alerts.
"matcher": "rate_limit"
PreToolUseToolsRuns before a tool call. Exit 2, or a JSON deny, cancels it in every permission mode. It can also ask you, or change the tool's input.
"matcher": "Bash"
PostToolUseToolsRuns after a tool call succeeds, such as to format a file Claude just edited. The tool has already run, so it cannot be undone.
"matcher": "Edit|Write"
PostToolUseFailureToolsRuns after a tool call fails. What the hook writes to stderr is shown to Claude.
"matcher": "Bash"
PostToolBatchToolsRuns once a batch of tool calls made together has finished, before Claude's next request. Exit 2 stops the loop there. Takes no matcher.
PermissionRequestToolsRuns when Claude Code is about to ask you to allow a tool call. A JSON decision can allow or deny it for you; exit 2 has no effect here.
"matcher": "Bash"
PermissionDeniedToolsRuns when auto mode denies a tool call. It cannot undo the denial, but its JSON can tell Claude it may try again.
"matcher": "Bash"
ElicitationToolsRuns when an MCP server asks you for input during a tool call. Exit 2 declines without showing a dialog. The matcher is the server's name.
"matcher": "github"
ElicitationResultToolsRuns after you answer an MCP server's request, before the answer is sent back. Exit 2 turns the answer into a decline.
"matcher": "github"
SubagentStartAgentsRuns when a subagent starts. The matcher is the subagent's type, such as Explore or one of your own.
"matcher": "Explore"
SubagentStopAgentsRuns when a subagent finishes. Exit 2 makes it keep working.
"matcher": "test-runner"
TaskCreatedAgentsRuns when a task is being added to the task list. Exit 2 takes the task back out.
TaskCompletedAgentsRuns when a task is about to be marked done. Exit 2 keeps it open, such as until the tests pass.
TeammateIdleAgentsRuns when a teammate in an agent team is about to go idle. Exit 2 keeps it working.
PreCompactContextRuns before the conversation is compacted, by you or automatically. Exit 2 blocks the compaction.
"matcher": "auto"
PostCompactContextRuns after compaction finishes. To put facts back into the conversation, use SessionStart with the compact matcher.
"matcher": "manual"
InstructionsLoadedContextRuns when a CLAUDE.md or .claude/rules file is loaded, at the start or later in the session. Useful to log which instructions applied.
"matcher": "path_glob_match"
FileChangedFilesRuns when a file you name in the matcher changes on disk, such as reloading the environment when .envrc changes.
"matcher": ".envrc|.env"
CwdChangedFilesRuns when the working folder changes, such as after Claude runs cd. Suits tools like direnv. Takes no matcher.
DirectoryAddedFilesRuns when a folder is added to the session, such as with /add-dir.
"matcher": "slash_command"
WorktreeCreateFilesRuns when a Git worktree is being created, and replaces Claude Code's own way of making it. Any non-zero exit makes the creation fail.
WorktreeRemoveFilesRuns when a worktree a WorktreeCreate hook made is being removed.
Questions
- What are hooks in Claude Code?
- Commands, web requests or prompts that Claude Code runs at fixed points in a session, such as before a tool runs, after a file is edited, when you send a prompt or when Claude stops. Claude Code runs them itself, so they happen every time rather than when Claude remembers to.
- How do I block a command with a hook?
- Add a PreToolUse hook with a matcher such as Bash. Your script reads the tool call as JSON on its input, and if the command should not run, writes the reason to stderr and exits with code 2. Claude Code cancels the call and shows Claude the reason.
- Where do I put Claude Code hooks?
- In the hooks block of a settings file - ~/.claude/settings.json for all your projects, .claude/settings.json to share with the team, or .claude/settings.local.json for yourself in one project. Plugins, skills and subagents can also carry hooks. Run /hooks to see every hook that is loaded.
- What is the difference between a hook and CLAUDE.md?
- CLAUDE.md is advice Claude reads and usually follows. A hook is a program Claude Code runs whatever Claude decides, so use it for things that must happen every time, such as formatting after each edit or refusing writes to a folder.
- Are hooks safe?
- A command hook runs with your full user permissions, so a hook in a repository you clone is code that runs on your machine. Claude Code asks you to trust a folder before its hooks run in an interactive session; read .claude/settings.json first, and in scripts with -p use --bare to skip a repository's hooks.
Sources