# Claude Code Hooks Tutorial: Automate Code Review

A Claude Code hooks tutorial using the real settings.json schema: wire a PostToolUse hook to a review skill. No fictional git-event or blocking fields.

From the book: [Claude Code Skills](https://youcanbuildthings.com/books/claude-skills.md)

Web page: https://youcanbuildthings.com/articles/claude-code-hooks-tutorial

> **Summary:**
> 1. The real hooks schema lives in settings.json, not .claude/hooks/*.json files.
> 2. A 13-line PreToolUse hook that blocks `rm -rf` before Claude can run it.
> 3. A PostToolUse hook that auto-runs a code-review skill on edits.
> 4. The real event names, from the official docs, and the fictional ones to delete.

Most every Claude Code hooks tutorial you'll find online uses a schema that doesn't exist: standalone `.claude/hooks/review.json` files, an `options.blocking: true` field, a `git:pre-commit` event. None of that is real. Hooks live in `settings.json`, the events are a fixed case-sensitive list, and a hook runs a shell command, not a skill. Get the schema right and you can make Claude review its own code automatically.

## What is a Claude Code hook?

A Claude Code hook is a shell command that fires on a Claude Code event. Three layers stack here: skills are the brain (`.claude/skills/<name>/SKILL.md`, defines behavior), hooks are the reflexes (`settings.json`, fire commands on events), MCP servers are the hands (`.mcp.json`, external data). This tutorial wires the first two so a review runs without you asking.

Hook configuration goes in a `hooks` block in one of:

- `~/.claude/settings.json` (all projects)
- `.claude/settings.json` (this project, commit it)
- `.claude/settings.local.json` (this project, gitignored)

Not `.claude/hooks/review.json`. The `.claude/hooks/` directory is a convention for the shell *scripts* a hook shells out to, never for the hook *config*.

## Hook 1: block `rm -rf` before Claude runs it

The most useful safety hook, and the smallest. In `.claude/settings.json`:

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(rm -rf *)",
            "command": "echo 'blocked: rm -rf is not allowed via Claude. Run it yourself if you really mean it.' >&2 && exit 1"
          }
        ]
      }
    ]
  }
}
```

Thirteen lines that stop Claude from nuking your home directory. When Claude is about to run a `Bash` tool whose command matches `rm -rf *`, the `if` predicate fires, the command exits non-zero, and the tool call is blocked. Claude sees the stderr message and stops. Note the schema: `type` is always `"command"`, there is an optional `if` predicate, and a `command`. There is no `action:` block, no `options.silent`, no `options.blocking`. If a tutorial shows those, it's wrong.

## The real event list (from the docs)

Hooks fire on a fixed, case-sensitive set of events. From the official [Claude Code hooks reference](https://code.claude.com/docs/en/hooks):

| Event | When it fires |
|---|---|
| `SessionStart` | When a session begins or resumes |
| `UserPromptSubmit` | When you submit a prompt, before Claude processes it |
| `PreToolUse` | Before a tool call executes. Can block it |
| `PostToolUse` | After a tool call succeeds |
| `PostToolUseFailure` | After a tool call fails |
| `Stop` | When Claude finishes responding |
| `PreCompact` | Before context compaction |
| `SessionEnd` | When a session ends |

For "react when Claude edits a file," the event is `PostToolUse` matched against `Edit|Write`. `file:save`, `git:pre-commit`, and `command:fail` are not on the list. They never were. Claude Code hooks are not git hooks: if you want a literal git pre-commit, that lives in `.git/hooks/pre-commit` and is not a Claude Code feature.

## Hook 2: auto-run a code-review skill on edits

Here's the automation. When Claude edits a file in a sensitive directory, fire a review using a skill. First the skill at `.claude/skills/code-review/SKILL.md`:

```markdown
---
name: code-review
description: Use this skill when reviewing code or diffs. Prioritizes bugs and security over style. One sentence per issue with a specific fix. Top-5 limit.
---

# Code Review
## Instructions
- Prioritize: bugs, security, performance, then API design
- State each issue in one sentence with a specific code fix
- Limit to 5 highest-priority items; no praise
```

Then the hook in `.claude/settings.json`:

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "if": "FilePath(src/security/**)",
            "command": "claude -p \"Review the staged changes to $CLAUDE_TOOL_FILE_PATH for security issues. Use the code-review skill. If clean, output: SECURITY-OK.\""
          }
        ]
      }
    ]
  }
}
```

This is how you "wire a hook to a skill." Not via a fictional `action.type: "skill"`. The hook command shells out to `claude -p` with a prompt that names the skill; the skill auto-invokes because its `description:` matches the prompt. The review prints to stdout and flows back into your active session.

![Three-layer architecture diagram: Skills as the brain in .claude/skills/SKILL.md, Hooks as the reflexes in settings.json, MCP servers as the hands in .mcp.json, with a real PreToolUse rm-rf-blocking hook and a real-versus-fictional hook event reference](https://youcanbuildthings.com/images/articles/claude-code-hooks-tutorial/hooks-architecture.jpg)


## What broke: hooks that silently never fire

Three failure points, in order of how often they bite:

1. **Wrong event name.** Events are case-sensitive. `PreToolUse`, not `pretooluse` or `preToolUse`. The most common mistake is using one of the fictional events (`file:save`, `git:pre-commit`) copied from an old blog post. Confirm yours is in the docs list above.
2. **Wrong location.** Config goes in the `hooks` block of `settings.json`. A `.claude/hooks/review.json` file is never read as hook config.
3. **Predicate too broad or too narrow.** `if: FilePath(**/*.ts)` fires on every TypeScript edit and burns tokens fast when the command calls `claude -p`. `if: FilePath(src/security/**)` fires only where it matters. Test predicates against real tool calls.

To debug a silent hook, redirect output while testing:

```bash
# Append to the hook command temporarily
... 2>&1 | tee /tmp/hook.log
# Then trigger the hook and read what actually ran
cat /tmp/hook.log
```

Cost note: hooks that don't call `claude -p` are free shell commands. Hooks that do cost tokens like any Claude turn (a 50-100 line review is a few hundred tokens, fractions of a cent). The lever is the `if` predicate. Scope it tight.

## What should you actually do?

- If you want one safety win today: add the `rm -rf` PreToolUse hook. Thirteen lines, immediate, free.
- If you want automated review: use the `PostToolUse` + `claude -p` + skill pattern, scoped with `if: FilePath(src/security/**)` so it only fires on sensitive code.
- If a hook isn't firing: check the event name against the docs list, confirm config is in `settings.json`, then narrow the predicate. Don't reach for a `blocking` field; it doesn't exist.

## bottom_line

- Hooks live in `settings.json` with real case-sensitive event names. `.claude/hooks/*.json`, `options.blocking`, and `git:pre-commit` are tutorial folklore.
- A hook runs a shell command. To involve a skill, that command calls `claude -p` and names the skill. There is no schema field that "runs a skill."
- Start with the `rm -rf` block, then scope a `PostToolUse` review hook tightly. Broad predicates that call `claude -p` are how hook costs sneak up on you.

## FAQ

### Where do Claude Code hooks go?

In a hooks block inside settings.json (~/.claude/settings.json, .claude/settings.json, or .claude/settings.local.json). Not in standalone .claude/hooks/*.json files.

### Is there a Claude Code git pre-commit hook event?

No. Claude Code hooks fire on Claude Code events like PreToolUse and PostToolUse, not git events. file:save, git:pre-commit, and command:fail do not exist.

### Can a hook invoke a skill directly?

Not via a schema field. The hook runs a shell command; to involve a skill, the command calls claude -p with a prompt that names the skill. There is no action.type skill field.

---

Get the book: Claude Code Skills, $12.99 on Amazon: https://amzn.to/4tbXnwe
