Hooks, explained by monkey
(plus 3 hook recipes)
A hook is your own command that Claude Code runs by itself at a set moment, so it happens every time instead of whenever Claude remembers. Here are three recipes to paste: auto-format, a .env guard and a ping when Claude needs you.
You can tell Claude to format your code after every change, and sooner or later it'll forget. A hook doesn't forget. It's a shell command you write into a settings file, and Claude Code runs it at a set moment every time, like right after Claude edits a file. Claude isn't the one deciding, so there's nothing to forget.
Where hooks go
Hooks go in a "hooks" block inside one of these settings files:
- ~/.claude/settings.json: all your projects.
- .claude/settings.json: this project. Commit it and your team gets the same hooks.
- .claude/settings.local.json: this project, just you.
Type /hooks inside Claude Code to see every hook you have. It's read-only, so to change or remove a hook, edit the file it lives in.
Recipe 1: format every file Claude edits
After Claude edits or writes a file, this runs Prettier on that file. Put it in .claude/settings.json in your project:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
What each part does:
- PostToolUse: run after a tool has run.
- "matcher": "Edit|Write": only after the tools Claude uses to edit and write files.
- The command: jq picks the edited file's path out of the details Claude Code hands the hook, and passes it to Prettier.
Recipe 2: keep Claude out of your .env file
This one runs before Claude edits or writes a file. If the file is a .env file, the hook writes a reason to stderr and exits with code 2. Exit 2 blocks the action, and the reason goes back to Claude, so it knows why it was stopped.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "f=$(jq -r '.tool_input.file_path'); case \"$(basename \"$f\")\" in .env|.env.*) echo \"Blocked: $f holds secrets. Don't edit .env files, ask the user instead.\" >&2; exit 2;; esac"
}
]
}
]
}
}
- PreToolUse: runs before a tool runs, and can stop it.
- The case part matches .env, plus anything starting .env. like .env.local. That includes .env.example, so delete |.env.* if you want Claude to edit those.
- >&2: sends the message to stderr, which is where Claude reads the reason from.
- exit 2: blocks the edit. Every other file goes through as normal.
Recipe 3: get a ping when Claude needs you
Walk away while Claude works, and get a Mac notification when it needs your attention. Put this in ~/.claude/settings.json so it works in every project:
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
}
]
}
]
}
}
All three in one file
A settings file gets one "hooks" block. To use more than one recipe in the same file, put each event inside that one block. Don't add a second "hooks" block. If the file already has other settings, "hooks" sits next to them. Here are all three together:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "f=$(jq -r '.tool_input.file_path'); case \"$(basename \"$f\")\" in .env|.env.*) echo \"Blocked: $f holds secrets. Don't edit .env files, ask the user instead.\" >&2; exit 2;; esac"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
],
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
}
]
}
]
}
}
Rather not touch JSON? Ask Claude
Claude can write a hook for you. Say what should happen and when:
Add a hook to .claude/settings.json that runs Prettier on every file you edit or write. Use a PostToolUse hook with the Edit|Write matcher. Keep everything already in the file, and if there's already a "hooks" block, add it inside that one.
Add a PreToolUse hook to .claude/settings.json that stops you from editing or writing any .env file. It should write the reason to stderr and exit with code 2.
Then type /hooks to check it's there.
Why a hook beats asking nicely
A rule in CLAUDE.md is advice: Claude reads it and usually follows it. A hook is enforced. The docs say a PreToolUse hook that denies a tool call blocks it even in bypass permissions mode or with --dangerously-skip-permissions. So if something must always happen, or must never happen, make it a hook.
The events worth knowing
- PreToolUse: before a tool runs. It can block it.
- PostToolUse: after a tool runs. Good for formatting and checks.
- Notification: Claude needs you.
- Stop: Claude finished replying.
- SessionStart: a session starts.
There are more than 30 events in all, like PreCompact, SubagentStop and SessionEnd. The hooks reference lists every one.
Gotchas
- Matchers are case-sensitive and must match the tool name exactly: Edit, Write, Bash. A lowercase edit won't match.
- Older tips use the matcher Edit|MultiEdit|Write. The current tools list has no MultiEdit tool, and the docs' own example uses Edit|Write.
- A PostToolUse hook can't undo anything, because the tool has already run. To stop something, use PreToolUse.
- Stop fires every time Claude finishes a reply, not only when a whole task is done.
- Hooks run your commands with your permissions, so only use hooks you trust. A command hook times out after 10 minutes by default.
- /hooks only shows your hooks. To remove one, delete its entry from the settings file.
Monkey believe in you. Now go build.
follow @nocodemonkey



