How to Set Up Claude Code Hooks (PreToolUse & PostToolUse, With Real Examples)

Last Verified: September 2026 — commands and maintenance status checked against live GitHub sources

Telling Claude Code “always run the formatter after editing” or “never run rm -rf” 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, 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": "npx prettier --write \"$CLAUDE_TOOL_INPUT_FILE_PATH\"" }
        ]
      }
    ]
  }
}

Start a new Claude Code session (hooks are snapshotted at startup) — every file it 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.

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 — Restart to load changes

~1 min

Claude Code snapshots hook configuration at session startup. Editing settings.json mid-session does nothing until you start a new session — this catches people every time.

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 milliseconds — hooks default to a 60-second timeout

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
PostToolUse hook 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, and whether the command itself runs correctly when you paste it into a terminal directly.

Common Mistakes to Avoid

  • Editing settings.json mid-session and expecting it to apply. Hooks are snapshotted at startup — start a new session after any change.
  • Wrong matcher casing. Tool names are case-sensitive: Write matches, write does not.
  • Expecting exit code 1 to block anything. Only exit code 2 blocks a PreToolUse call — 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

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *