Claude Code Hooks: A Complete Guide with Real Examples
Hooks are the most under-documented feature in Claude Code. They are also the single biggest productivity unlock once you actually understand them.
Claude Code Hooks: A Complete Guide with Real Examples
Hooks are the feature everyone scrolls past in the Claude Code docs and later wishes they had found sooner. They are the single biggest productivity unlock in the tool, and almost nobody talks about them. This post is the version of the docs I wish existed when I started.
What a hook actually is
A hook is a shell command that Claude Code runs automatically when a specific event fires. You configure them in your settings.json (either the global one at ~/.claude/settings.json or a project-level .claude/settings.json), and from that point on Claude Code takes care of running them whenever the matching event occurs.
There are five hook events worth knowing about:
- PreToolUse — runs before a tool call. You can inspect what Claude is about to do and block it.
- PostToolUse — runs after a tool call. Useful for formatting, linting, reloading state.
- UserPromptSubmit — runs when you hit enter on a prompt. Good for logging or auto-prepending context.
- Stop — runs when Claude finishes a response. Perfect for notifications or session summaries.
- SessionStart — runs when a new session begins. Use this to print status, inject project context, or warm caches.
The flow is simple: event fires, Claude Code executes your shell command, and depending on the exit code and output, either continues or blocks.
Your first hook: auto-format after every edit
Here is the hook I run on every project. After Claude edits a file, it gets auto-formatted. No more asking Claude to "please run prettier after you save." No more committing unformatted code when Claude forgets.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "npx prettier --write \"$CLAUDE_TOOL_FILE_PATH\" 2>/dev/null || true"
}
]
}
]
}
}
The matcher is a regex against the tool name. Edit|Write catches both the Edit and Write tools. The $CLAUDE_TOOL_FILE_PATH environment variable gets injected by Claude Code and points at the file that was just modified. The || true swallows errors so a Prettier failure on a non-JS file does not abort the edit.
That is a 10-line config change that saves me dozens of "please format that" prompts every week.
Hook #2: block destructive commands
Claude Code ships with built-in permission prompts for dangerous commands. But sometimes you want an absolute block, not a prompt. PreToolUse hooks can do that.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo \"$CLAUDE_TOOL_INPUT\" | grep -qE \"rm -rf|git push --force|DROP TABLE\" && { echo \"blocked: destructive command\"; exit 2; } || exit 0"
}
]
}
]
}
}
Exit code 2 tells Claude Code to block the tool call and feed your stderr output back to the agent as feedback. Claude will see "blocked: destructive command" and either rephrase or tell you why it needed to run that.
This is not security — Claude can work around it if you prompt it to. It is ergonomics. It catches the 99% case where you fat-fingered a prompt.
Hook #3: desktop notification when a long task finishes
Some Claude Code sessions run for 45 minutes. I do not want to sit and stare. Stop hook to the rescue:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude finished\" with title \"Claude Code\" sound name \"Submarine\"'"
}
]
}
]
}
}
On macOS that triggers a real notification with sound. On Linux swap for notify-send. On Windows use burnttoast or similar.
Hook #4: session-start project briefing
When I open a new session in a project, I want a status snapshot. SessionStart hook:
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "echo \"=== Project status ===\"; git log --oneline -5; echo; echo \"=== Changed files ===\"; git status --short"
}
]
}
]
}
}
Everything the hook prints to stdout gets injected into Claude's context at session start. Claude sees the git state before you even say a word. This is the cheapest possible way to keep your agent oriented.
Hook #5: log every prompt for review
Want to keep a record of every prompt you send, for auditing or for your own future reference? UserPromptSubmit hook:
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "echo \"[$(date -Iseconds)] $CLAUDE_USER_PROMPT\" >> ~/.claude/prompt-log.txt"
}
]
}
]
}
}
Every prompt appended, timestamped. Three months later you can grep for "refactor auth" and find exactly when you asked Claude to do that and in what context.
Where people get hooks wrong
Three mistakes I see constantly:
- Forgetting the matcher is a regex.
EditmatchesEditandEditNotebook. If you want only one, anchor it:^Edit$. - Blocking on non-zero exit. Exit code 1 does not block — only exit code 2 does. Everything else is treated as "ran, but whatever, continue."
- Writing heavy commands inline. If your hook is more than one line, put it in a shell script at
.claude/hooks/format.shand point the hook at that file. Easier to read, easier to version control, easier to debug.
The bigger idea
Hooks are the glue that turns Claude Code from "a chat window that writes code" into a real development environment. They are how you enforce conventions, ship telemetry, auto-format, run tests, send notifications, inject context, and build workflows that survive across sessions.
MOLTamp uses hooks under the hood for skin-reactive state changes — when Claude starts thinking, a PreToolUse hook fires and updates the skin's data attributes so the border glow intensifies. When it finishes, a PostToolUse hook dims it back down. The hooks framework is what makes that possible.
If you take one thing from this post: go open your ~/.claude/settings.json right now and add the PostToolUse prettier hook. It is a two-minute config change that pays off on literally every session.