Taste is mostly consistency. You never use certain words. Published work stays frozen. Every draft ends with a word count. You can write all of that in CLAUDE.md, and Claude will follow it most of the time. "Most of the time" is the problem.
Hooks close the gap. A hook is a command (or a short prompt) that Claude Code runs automatically at a set moment, such as right before Claude edits a file or right after it finishes replying. Claude doesn't decide whether to run it. The software does, every time.
What a hook can be
There are five kinds of hook handler:
- command: runs a shell command or script. The workhorse.
- prompt: asks a Claude model a yes-or-no question about what's happening. Good for judgment calls.
- http: sends the event to a web address.
- mcp_tool: calls a tool on a connected MCP server.
- agent: starts a subagent that can read files before deciding. Anthropic labels this one experimental.
This post sticks to command and prompt hooks.
The moments you can hook
Claude Code fires hook events at points in a session. These are the ones you'll use most:
| Event | When it fires | Can it stop things? |
|---|---|---|
SessionStart | A session begins or resumes | No, but it can add context |
UserPromptSubmit | You send a prompt, before Claude sees it | Yes, it can reject the prompt |
PreToolUse | Before a tool runs (edit, command, fetch) | Yes, it can block the tool |
PostToolUse | After a tool succeeds | No, but it can send Claude feedback |
Notification | Claude Code sends a notification, like needing approval | No |
Stop | Claude finishes responding | Yes, it can keep Claude working |
SubagentStop | A subagent finishes | Yes |
SessionEnd | The session ends | No |
The full list is longer: Setup, UserPromptExpansion, PermissionRequest, PermissionDenied, PostToolUseFailure, PostToolBatch, MessageDisplay, SubagentStart, TaskCreated, TaskCompleted, StopFailure, TeammateIdle, InstructionsLoaded, ConfigChange, CwdChanged, DirectoryAdded, FileChanged, WorktreeCreate, WorktreeRemove, PreCompact, PostCompact, PreModelSwitch, PostModelSwitch, Elicitation and ElicitationResult. The hooks reference documents each one.
Where hooks live
Hooks go in the same settings files as your permissions:
~/.claude/settings.jsonfor hooks that follow you into every project.claude/settings.jsonfor a project's hooks, committed so collaborators get them.claude/settings.local.jsonfor your personal hooks in one project
Plugins, skills and subagents can carry hooks too. Every hook has three layers: the event, a matcher that narrows when it fires, and one or more handlers to run. For tool events, the matcher is the tool's name: Bash, Edit|Write for either of two tools, or a pattern like mcp__notion__.* for every tool on one MCP server. Matchers are case-sensitive.
Type /hooks in a session to browse every hook you have and where each one comes from. Or describe the hook you want in plain English and ask Claude to add it, then read what it wrote.
How a hook says no
A command hook receives details about the event as JSON (structured text) on its input. For a file edit, that includes tool_input.file_path; for a shell command, tool_input.command. Then its exit code, the number a program returns when it finishes, decides what happens:
- Exit 0: all good. The action goes ahead.
- Exit 2: stop. Whatever the hook printed to its error output (stderr) goes to Claude as the reason. On
PreToolUsethe tool is blocked. OnPostToolUsethe tool already ran, so Claude just reads the message and can fix things. OnStop, Claude keeps working. - Any other code, including 1: a non-blocking error. You see a notice and the action goes ahead anyway.
That last one catches people out. If a hook is meant to enforce something, it must exit 2. For finer control, a PreToolUse hook can print JSON instead, with a permissionDecision of allow, deny, ask or defer and a reason.
The scripts below use jq, a small tool for reading JSON. Install it with brew install jq on macOS or apt-get install jq on Debian and Ubuntu.
Four hooks worth stealing
1. Ping me when Claude needs me
Stop watching the terminal. Add this to ~/.claude/settings.json:
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
}
]
}
]
}
}
That's macOS. On Linux, swap the command for notify-send 'Claude Code' 'Claude Code needs your attention'. An empty matcher fires on every notification; use permission_prompt to hear only about approvals, or idle_prompt for "finished a while ago and you haven't replied".
2. Keep Claude out of a folder, and say why
Say published/ holds work that's live. A deny rule like Edit(/published/**) would block edits on its own. A hook goes further: it tells Claude what to do instead.
#!/bin/bash
# Blocks edits to published/ and points Claude at drafts/.
FILE=$(jq -r '.tool_input.file_path // empty')
case "$FILE" in
*/published/*)
echo "published/ is frozen. Copy the file to drafts/ and edit the copy." >&2
exit 2
;;
esac
exit 0
Make it runnable with chmod +x .claude/hooks/protect-published.sh, then register it:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-published.sh"
}
]
}
]
}
}
This hook watches Claude's file tools, not shell commands like mv. Keep the deny rule too: it also covers the shell file commands Claude Code recognizes, such as sed and tee. Belt and braces.
3. A style check after every edit
This is the taste one. After Claude writes a Markdown file, check it against your banned-words list. If anything turns up, exit 2, and Claude sees exactly which lines to rewrite.
#!/bin/bash
# Flags banned words in Markdown files Claude just wrote.
FILE=$(jq -r '.tool_input.file_path // empty')
case "$FILE" in
*.md|*.mdx) ;;
*) exit 0 ;;
esac
HITS=$(grep -n -i -w -E 'delve|game-changer|supercharge|seamless|revolutionary' "$FILE")
if [ -n "$HITS" ]; then
echo "Style check failed. Rewrite these lines without the banned words:" >&2
echo "$HITS" >&2
exit 2
fi
exit 0
Register it like example 2, but under PostToolUse instead of PreToolUse. Edit the word list to match your own. If your project has code, the same pattern runs a formatter: Anthropic's guide shows a one-line PostToolUse hook that runs Prettier on every file Claude edits.
4. A checklist before Claude calls it done
A Stop hook fires when Claude finishes replying. A prompt hook asks a Claude model to check the reply, and $ARGUMENTS is replaced with the event details, including Claude's last message:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Here is what just happened: $ARGUMENTS. If Claude edited a draft in this turn, its final message must list the files it changed, flag any claim that needs a source, and give the word count. If any of those is missing, respond with {\"ok\": false, \"reason\": \"what to add\"}. Otherwise respond with {\"ok\": true}."
}
]
}
]
}
}
If the check returns "ok": false, Claude gets the reason and keeps going. Two cautions. Stop fires every time Claude finishes a reply, not only at the end of a task, which is why the prompt starts with "if Claude edited a draft". And each check is a small extra model call, so it adds a little cost. Claude Code also steps in if a stop hook blocks eight times in a row, so a badly written check can't loop forever. For a one-off version, the built-in /goal command sets up the same kind of check for the current session.
Be careful: hooks run as you
Anthropic's warning is blunt: command hooks "execute shell commands with your full user permissions". They can read, change or delete anything your account can, and they run outside Claude Code's sandbox.
- Read every hook before you add it, including ones Claude writes for you.
- In a project you didn't write, open its
.claude/settings.jsonbefore you trust the folder. Interactive sessions hold hooks back until you accept the trust dialog, but scriptedclaude -pruns don't show that dialog. Add--settings '{"disableAllHooks": true}'to such a run to switch hooks off. - In scripts, quote your variables (
"$FILE", not$FILE), reference files through$CLAUDE_PROJECT_DIR, and skip sensitive files like.env. - Keep hooks fast. A slow
PreToolUsehook slows down every single edit. - To pause everything, set
"disableAllHooks": truein a settings file.
Hooks are the enforcement layer. Pair them with permission rules for hard boundaries and skills for the know-how, and you have an agent that works the way you do, without you repeating yourself.
