Hooks

Claude Code hooks: guardrails that run every single time

Hooks are commands Claude Code runs at fixed moments, like before a tool call. How to configure them, block dangerous commands, and auto-format edited files.

You can ask Claude in CLAUDE.md never to delete your home directory. It will almost always listen. A hook makes it certain.

Instructions are advice, hooks are rules

Everything in CLAUDE.md is guidance the model weighs up. A hook is code that Claude Code itself runs at a fixed moment: before a tool runs, after a file is edited, when a session starts. The model can't skip it or talk its way past it. That makes hooks the right tool for anything that must always happen, or must never happen.

Where hooks are configured

Hooks live in the same settings files as everything else:

File Scope
~/.claude/settings.json You, in every project
.claude/settings.json This project, shared with the team through git
.claude/settings.local.json You, in this project only (not committed)

The shape is always event, then a matcher, then the commands to run:

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "python3 ~/.claude/hooks/block-rm.py" }
        ]
      }
    ]
  }
}

For tool events the matcher is the tool name. It accepts alternatives and regular expressions, such as Edit|Write, and it's case-sensitive. An empty matcher matches everything.

The events worth knowing first

There are over thirty events. These cover most real use:

Event Runs when
SessionStart A session starts or resumes
UserPromptSubmit You send a prompt, before Claude reads it
PreToolUse Before a tool runs. Can block it
PostToolUse After a tool succeeds
Stop Claude finishes responding
PreCompact Before the conversation is compacted

How a hook talks back

Claude Code sends the hook a JSON description of the event on stdin. For tool events that includes tool_name and tool_input, so a Bash hook can read the exact command at tool_input.command.

The hook answers with its exit code:

  • 0 means carry on.
  • 2 means block. Whatever the hook printed to stderr is passed back as the reason, so Claude can adjust.
  • Anything else is a non-blocking error: the action continues and you see a notice.

Example: block a destructive delete

Save this as ~/.claude/hooks/block-rm.py and register it with the PreToolUse configuration above:

python
#!/usr/bin/env python3
import json
import re
import sys

event = json.load(sys.stdin)
command = event.get("tool_input", {}).get("command", "")

# rm with a recursive flag, aimed at / or ~ themselves.
DANGEROUS = re.compile(r"\brm\s+-\w*[rR]\w*\s+(/|~)(\s|$)")

if DANGEROUS.search(command):
    print("Blocked: recursive delete of / or ~. Delete a specific path instead.", file=sys.stderr)
    sys.exit(2)

sys.exit(0)

Test it without Claude by piping in a fake event:

bash
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf ~"}}' | python3 ~/.claude/hooks/block-rm.py
echo $?   # 2 means it would have been blocked

Example: format every file Claude edits

A PostToolUse hook on Edit|Write can run your formatter on the file that just changed, so you never review unformatted code:

json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write" }
        ]
      }
    ]
  }
}

Before you add a hook

  • Hooks run as you. They have your permissions, so read any hook script before installing it, the same as any script from the internet.
  • Keep them fast. A PreToolUse hook runs before every matching tool call. Command hooks time out after 10 minutes by default, but you want them to finish in well under a second.
  • Allowing isn't a bypass. A hook can block a tool, but a hook that approves one doesn't override your permission rules.

Questions

Should a rule go in a hook or in CLAUDE.md?

If breaking it would be costly, such as deleting data, leaking secrets or pushing to the wrong branch, use a hook. If it's a preference about style or approach, CLAUDE.md is enough.

Can my whole team use the same hooks?

Yes. Put them in the project's .claude/settings.json and commit it, with the hook scripts in the repository.

What's the difference between exit code 1 and exit code 2?

Exit code 2 blocks the action and sends your message to Claude. Exit code 1, like any other non-zero code, is treated as a hook error: the action still goes ahead.

Sources

Field notes

Get the next guide by email.

Practical notes on getting more out of Claude Code: skills, hooks, MCP and agent workflows, plus what changes in each Overclaud release. Free, by email.

No spam. Unsubscribe any time. Privacy.