Last Verified: October 25, 2026 — commands checked against live GitHub sources, plus a real hook rewrite measured on our own machine
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:
1. Show me the exact hooks block you added.
2. Make a small test edit to a real file in this project.
3. Show me the hook actually ran (its output or the resulting file change) as a direct result of that edit.
4. 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.

On this page (7 sections)
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: PreToolUse runs before a tool executes and can block it; PostToolUse runs after and can only react. Use PreToolUse for guardrails, PostToolUse for 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.

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 |
Measured by us · October 25, 2026
This isn’t a made-up example. It’s the PreToolUse hook we run on our own laptop for every Claude Code session, captured live today.
| What we checked | Result |
|---|---|
The hook entry in our ~/.claude/settings.json | {"matcher": "Bash", "hooks": [{"type": "command", "command": "rtk hook claude"}]} under PreToolUse — rewrites the Bash command before it runs, instead of waiting for the output and trimming after |
| A real command it rewrote just now | We sent {"tool_name":"Bash","tool_input":{"command":"git log --oneline -20"}} on stdin (the exact JSON Claude Code sends). The hook returned updatedInput.command = "rtk git log --oneline -20" — it swaps the command for a token-aware version before Claude Code ever runs it |
| Same folder listing, with and without the hook’s rewrite | Native ls -la on this project’s folder: 117 lines, 7,734 bytes. The same listing through rtk ls -la: 52 lines, 1,451 bytes — about 81% smaller, same information (permissions, size, name), run back-to-back on this machine |
This is one hook (a command rewriter), not the formatter example above — both are PreToolUse/PostToolUse hooks doing different jobs. We didn’t estimate the byte counts; they’re wc -c on the actual command output.
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
✘ 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, and 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 PostToolUse hooks 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/PostToolUse hooks 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.json overall
If this guide got your first hook actually blocking or automating something, follow us on X for more setup guides and prompt tips.

Comments
5 responses to “Your Prompt Rules Are Just Suggestions: Claude Code Hooks Auto-Format Every Edit and Block Dangerous Commands (2-Minute Setup)”
[…] labels hooks as an alpha feature. New to hooks? Our Claude Code hooks guide explains how PreToolUse and SessionStart […]
[…] The agent can do anything your account can. Keep manual approval on for create and update tools; a Claude Code hook can enforce […]
[…] One tool covers every POST, PATCH, PUT, and DELETE. Keep manual approval on for it. A Claude Code hook can enforce […]
[…] to /mcp, keep manual approval on for tools that create or update, so you see each change first. A Claude Code hook can enforce […]
[…] that text. Supabase wraps SQL results in warnings, but keep manual approval on for execute_sql. A Claude Code hook can enforce […]