# Hooks: a rule that cannot be forgotten

Course: Claude Code: Pro Level — https://zero2vibecode.com/learn/claude-code-pro
Canonical URL: https://zero2vibecode.com/learn/claude-code-pro/2-1-pretooluse-hooks
Verified against: Claude Code v2.1.292 (2026-10-15)
Last updated: 2026-10-15

A rule in CLAUDE.md is advice Claude can miss. A hook is your own command that Claude Code runs at a set moment, every time. A PreToolUse hook that exits with code 2 blocks the action before permissions are even checked.

You write in CLAUDE.md: "never edit package-lock.json". Claude follows it almost always. But "almost" is exactly the problem: a rule in memory is advice the model can miss in a long session.

A hook works differently. It is **your own command** that Claude Code runs at a set moment in the session, **every time** — whatever Claude decides.

## Hook events

A hook is attached to an event. The main ones:

| Event | When it fires |
|---|---|
| `SessionStart` | when the session starts |
| `UserPromptSubmit` | when you send a request |
| `PreToolUse` | before every tool call |
| `PostToolUse` | after a successful tool call |
| `Stop` | when Claude finishes answering |
| `Notification` | when Claude Code needs your attention |

Today we care about `PreToolUse` — the hook that can **stop** an action.

## How PreToolUse blocks

Before a tool is called, Claude Code runs your script and feeds it JSON describing the event — which file is about to be edited, for example. The exit code decides what happens next:

- **0** — all clear, the action goes ahead;
- **2** — the call is **blocked**, and whatever the script printed to stderr goes to Claude as the reason.

Claude sees the explanation — say, "Blocked: package-lock.json is protected" — and changes its plan.

One important detail: `PreToolUse` fires **before** the permission mode is checked. So the block holds in every mode, Auto included.

## Registering a hook

Hooks are declared in a settings file, next to the rules from the previous level:

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/protect-files.sh", "args": [] }
        ]
      }
    ]
  }
}
```

- `matcher` picks the tools: `Edit|Write` means file edits and writes only;
- `${CLAUDE_PROJECT_DIR}` fills in the project path, so the hook is found from anywhere;
- with `"args": []` the script runs directly, without a shell, and the path does not need quoting.

Inside a session, `/hooks` shows which hooks were picked up.

You can test the script **without Claude**: feed it the JSON Claude Code would send and look at the exit code.

```bash
echo "{\"tool_input\":{\"file_path\":\"$PWD/package-lock.json\"}}" | .claude/hooks/protect-files.sh; echo "exit $?"
```

If you get `exit 2`, the hook will fire in a session too.

**Careful.** 
Hooks run with your user's rights: in an interactive session, after you have trusted the folder, and in claude -p without any question at all. Before you trust someone else's repository, or run Claude in it from a script, read its hooks.

**Practice in the terminal.** Create a .claude/hooks folder and a protect-files.sh script, then register it in .claude/settings.json on the PreToolUse event with the matcher Edit|Write. In the practice terminal this is just writing settings — in a real project, the script itself decides whether to block the edit.
