Hooks & Automation
Guarantees, not suggestions — a hook runs every time, whether or not Claude remembers to.
Beginner → Advanced
settings.json files as everything else in
Claude Code CLI Basics: put this in
.claude/settings.json for the whole project, or
~/.claude/settings.json if you just want it for yourself, everywhere.
{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "notify-send 'Claude is done' 'Check the terminal.'" }
]
}
]
}
}
notify-send is the Linux desktop-notification command (see
Ubuntu terminal commands); swap in
afplay /System/Library/Sounds/Glass.aiff on macOS if you'd rather hear a
sound. Notice there's no matcher here — Stop fires once when
Claude finishes responding, it isn't scoped to a particular tool, so there's nothing to
match against.
| Event | Fires | Typical use |
|---|---|---|
| PreToolUse | before a tool call runs | block a dangerous command outright, or require confirmation for a pattern |
| PostToolUse | after a tool call succeeds | auto-format a file the moment it's edited, log every command run |
| UserPromptSubmit | before Claude processes what you typed | inject extra context, or reject a prompt outright |
| SessionStart | when a session begins or resumes | print a status banner, load environment-specific context |
| Stop | when Claude finishes responding | run the full test suite before letting the turn actually end |
| SubagentStop | when a subagent finishes | validate a subagent's output before its summary reaches the main session |
settings.json/settings.local.json
files covered in Claude Code CLI Basics — project-level
hooks are meant to be committed, exactly like a repo's git hooks. A matcher
scopes a hook to specific tools (e.g. only Bash) using a tool name, several
names separated by |, or a regex.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh" }
]
}
]
}
}
/hooks/hooks in a session to open a browser of every hook event, with a count next
to any event that has something configured; pick an event and it shows each hook's
matcher, command, and which settings file it came from — project, user, local, a managed
policy, even one bundled with a plugin or skill.
/hooks won't add, change,
or remove anything; for that you're back to editing the settings file yourself, or asking
Claude to do it. Edits to a settings file are picked up automatically within a few seconds
while Claude Code keeps running, so there's no restart-and-recheck loop either way.
PostToolUse,
matched to the two tools that touch files):
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/format.sh" }
]
}
]
}
}
#!/bin/bash
FILE=$(jq -r '.tool_input.file_path')
case "$FILE" in
*.py) black "$FILE" ;;
*.js|*.ts) npx prettier --write "$FILE" ;;
esac
SessionStart) — whatever the command prints to stdout gets added to Claude's
context, so this is a cheap way to save it from having to run git status
itself on turn one:
{
"hooks": {
"SessionStart": [
{
"hooks": [
{ "type": "command", "command": "git branch --show-current && git status --short" }
]
}
]
}
}
PreToolUse, scoped to
Bash only) — this is the one worth reading closely, since it's the first
example on this page that actually stops something rather than reacting to it:
#!/bin/bash
COMMAND=$(jq -r '.tool_input.command')
if echo "$COMMAND" | grep -q 'rm -rf'; then
echo "Blocked: rm -rf is not allowed by this project's hooks." >&2
exit 2
fi
0 means allow,
2 means block and treat stderr as the rejection reason shown to Claude, any
other code is a non-blocking error. That's what the block-rm.sh script above
is doing. A PreToolUse hook can also return structured JSON with an explicit
permissionDecision of deny, allow, or ask
instead of relying on the exit code alone.
PostToolUse fires after the tool already ran,
so the format-on-save example above can fix a file but never prevent the edit. PreToolUse,
UserPromptSubmit, and Stop are the ones where blocking actually
makes sense.
--no-verify past. Start narrow:
enforce the one or two things that have actually caused a real problem before, not every
theoretical one.