Your Prompt Rules Are Just Suggestions: Claude Code Hooks Auto-Format Every Edit and Block Dangerous Commands (2-Minute Setup)
You told Claude Code “always run the formatter after editing” and “never run rm -rf ”. It listened, until it didn’t. A rule in a prompt is a request - it can be forgotten, skipped, or missed after a context compaction. A hook is not a request. It’s a shell command that runs automatically at a specific point in the agent’s workflow, and it can actually block an action before it happens. This guide covers real, practical hook setups you can have running in about 2 minutes, not just the concept.
Quick Start
The single most useful starter hook: auto-format every file Claude Code edits, so formatting is never something you have to ask for. Add this to .claude/settings.json in your project root:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
Save the file. Claude Code watches its settings files and picks up the hook in the running session (run /hooks to confirm it’s listed). The hook reads the edited file’s path from the JSON Claude Code sends on stdin, so it needs jq installed. Every file Claude writes or edits now gets auto‑formatted, no reminder needed. Total time: about 2 minutes.
⚡ Copy This Prompt: Let Claude Code Write and Verify the Hook For You
Skip the manual JSON editing and hand the whole thing to the agent instead:
Add a PostToolUse hook to this project's .claude/settings.json that runs [DESCRIBE WHAT YOU WANT, e.g. "prettier --write on every file the Write or Edit tool touches" or "eslint on every .ts file after it's edited"]. Then do the following to prove it actually works, not just that the JSON is valid:
- Show me the exact hooks block you added.
- Make a small test edit to a real file in this project.
- Show me the hook actually ran (its output or the resulting file change) as a direct result of that edit.
- If it didn't fire, tell me why (wrong matcher, wrong event, syntax error in
settings.json) instead of claiming it worked.
This works because Claude Code can edit its own settings file, then immediately trigger the hook by making a real edit and checking the result - it verifies the hook fires, not just that the config parses.
What You’ll Need
| Requirement | Why You Need It | Time |
|---|---|---|
| Claude Code installed | Hooks are a built‑in Claude Code feature - no extra install | 0 min |
A .claude/settings.json file |
Where project‑scoped hooks live; create it if it doesn’t exist yet | ~1 min |
| The command your hook runs | A formatter, linter, or shell script already available on your system | varies |
Step‑by‑Step Setup
Prefer to build your own hook from scratch instead of the starter above? Here’s how the pieces fit together.
Step 1 - Pick where the hook lives (~1 min)
~/.claude/settings.json applies to every project on your machine. .claude/settings.json in a repo root is project‑scoped and can be committed to git, so your whole team gets the same automation. Both levels combine rather than override - every matching hook from every scope runs.
Step 2 - Pick the event (~1 min)
Two events cover most real use cases:
PreToolUseruns before a tool executes and can block it.PostToolUseruns after and can only react.
UsePreToolUsefor guardrails,PostToolUsefor automation like formatting or logging.
Step 3 - Write a blocking guardrail (PreToolUse) (~2 min)
A hook script that inspects a Bash command and exits with code 2 blocks it before it runs. Matchers are case‑sensitive tool names - Bash works, bash does not:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/path/to/block-dangerous-commands.sh"
}
]
}
]
}
}
Inside that script, checking stdin for a pattern like rm -rf and exiting 2 if found is enough to stop the tool call outright - exit code 1 or any other non‑zero code is just logged as an error and doesn’t block anything.
Step 4 - Confirm it loaded (~1 min)
Claude Code watches its settings files and applies hook edits to the running session. Run /hooks to confirm the hook is listed. If it isn’t there after a few seconds, check the JSON for trailing commas, then restart the session to force a reload. Hooks run every time: tidy each edit, and stop the dangerous command before it runs.
What Each Piece Does
| Piece | What It Does |
|---|---|
| matcher | Regex matched against the tool name (case‑sensitive) - controls which tool calls trigger the hook |
| PreToolUse | Fires before a tool runs; exit code 2 blocks the call entirely |
| PostToolUse | Fires after a tool already ran; exit code 2 surfaces a blocking error to Claude, but the action already happened |
| type: "command" | Runs a shell command, receiving JSON context about the event on stdin |
| timeout | Optional field, in seconds - command hooks default to 10 minutes, so set a shorter one for anything that might hang |
Verify a Hook Actually Fires
Config validity is not proof it works. Trigger the exact tool the matcher targets and confirm the side effect happened:
echo "test" >> some-file.txt && cat some-file.txt
- ✔
PostToolUsehook fired on Write - formatter output shown - ✘ Hook did not fire - check matcher casing and event name
- ! Hook fired but silent - check the command’s own exit code and stderr
If nothing happens, check three things in order:
- The matcher’s exact casing.
- Whether you restarted the session after editing
settings.json. - Whether the command itself runs correctly when you paste it into a terminal directly.
Common Mistakes to Avoid
- Reading the file path from an environment variable. Claude Code passes the tool call as JSON on stdin; extract the path with
jq -r '.tool_input.file_path'. - Wrong matcher casing. Tool names are case‑sensitive:
Writematches,writedoes not. - Expecting exit code 1 to block anything. Only exit code 2 blocks a
PreToolUsecall - any other non‑zero code is just logged as an error while the tool still proceeds. - Assuming
PostToolUsehooks run in order. Multiple hooks matching the same event run simultaneously, not sequentially - don’t rely on one finishing before another starts. - Forgetting subagents inherit hooks too. If Claude spawns a subagent via the Agent tool, your
PreToolUse/PostToolUsehooks fire for every tool call the subagent makes as well.
Q&A
Can a hook actually stop Claude from running a command?
Yes - a PreToolUse hook that exits with code 2 blocks the tool call before it executes. This is the only way to get a hard guarantee, versus a prompt instruction the agent might not always follow.
What’s the difference between project and user scope?
User‑level (~/.claude/settings.json) applies everywhere on your machine. Project‑level (.claude/settings.json) is scoped to one repo and can be committed so your whole team shares the same automation. Both run together, not one‑or‑the‑other.
Do hooks slow down every tool call?
Only for the tools their matcher targets, and only by however long the command takes - keep hook commands fast, and set an explicit timeout for anything that might run long.
Can I log every tool call without blocking anything?
Yes - a PostToolUse hook that just appends to a log file and always exits 0 records activity without affecting the agent’s behavior at all.
Where can I see all the available hook events?
The official reference at code.claude.com/docs/en/hooks lists the full set beyond PreToolUse/PostToolUse, including events for prompt submission and session lifecycle.
Official Resources
- Claude Code Hooks Reference - full list of events, matchers, and exit code behavior
- Claude Code Settings Documentation - where hooks fit into
settings.jsonoverall - Originally published at quickpromptco.com, where the guide is kept up to date.
Comments
No comments yet. Start the discussion.