tutorial from: Claude Code Skills

How to Automate Code Review with Claude Code Hooks

by J Cook · 8 min read·

Summary:

  1. Wire a pre-commit hook that runs Claude’s code review on every commit automatically.
  2. Get the complete hook JSON config and skill file, ready to drop in.
  3. Real results: 14 bugs caught in 2 months, including 3 security issues and 1 SQL injection.
  4. Copy-paste configs for two bonus hooks: auto-test on save and documentation sync.

Skills tell Claude what to do. Hooks tell Claude when to do it. Without hooks, you manually ask Claude to review every file. With a pre-commit hook, Claude reviews automatically before every commit. You didn’t ask. The hook saw the event, loaded the skill, ran the review.

I ran this for two months. 14 bugs caught before they hit git history. Three were security issues. One was a SQL injection.

How do you set up a pre-commit hook?

Step 1: Create the hooks directory.

mkdir -p .claude/hooks

Step 2: Create the code review skill at .claude/skills/code-review.md (if you don’t have one from the 5-skill starter):

# Code Review

## Trigger
When reviewing code, pull requests, or diffs.

## Instructions
- Priority order:
  1. Bugs and correctness issues
  2. Security concerns
  3. Performance (only if measurable impact)
  4. API design and naming
- For each issue: state problem, explain risk, suggest fix
- If code is fine, say "No issues found" and stop.

## Constraints
- Do not comment on formatting if a linter handles it
- Limit to 5 highest-priority items

Step 3: Create the hook at .claude/hooks/pre-commit-review.json:

{
  "name": "pre-commit-review",
  "event": "git:pre-commit",
  "pattern": "**/*.{ts,tsx,js,jsx}",
  "action": {
    "type": "skill",
    "skill": "code-review",
    "prompt": "Review the staged changes. Focus on bugs and security issues only. If no issues found, approve silently."
  },
  "options": {
    "silent": true,
    "blocking": true
  }
}

Step 4: Stage a file and commit. Watch the terminal. Claude reviews before the commit goes through.

That’s it. Four steps, two files, Claude now reviews every commit. Here’s what it looks like in your terminal:

$ git add src/api/users.ts
$ git commit -m "feat(api): add user registration endpoint"

[hook: pre-commit-review] Reviewing staged changes...

⚠ Issue 1/2 (security): Input validation missing
  src/api/users.ts:14 — req.body.email is used directly
  in the database query without validation.
  Risk: Malformed input causes DB errors or injection.
  Fix: Add zod schema validation before the query:
    const body = registrationSchema.parse(req.body)

⚠ Issue 2/2 (bug): Null reference risk
  src/api/users.ts:28 — user.profile.name accessed without
  checking if profile exists.
  Fix: Add optional chaining: user.profile?.name

[hook: pre-commit-review] 2 issues found. Commit proceeds.

You see the issues before the commit lands. Fix them now or fix them after a bug report. Your call.

What did the hook actually catch?

14 bugs in two months. Here’s the breakdown:

CategoryCountExample
Null reference risks5Accessing property without checking for undefined
Missing input validation3API endpoints accepting unvalidated user input
Race conditions2Async handlers with shared state
Silent error swallowing2Catching errors and doing nothing with them
SQL injection1String concatenation in a query builder
Type coercion bug1Loose comparison in a number-to-string check

The SQL injection catch alone justified the hook. That would have been a real vulnerability in production. The pre-commit hook found it because the code review skill includes “check for string concatenation in database queries” as a priority item.

Cost in extra commit time: 5-10 seconds per commit. Worth it.

Architecture diagram showing skills define behavior, hooks trigger actions, MCP servers provide data

What broke (and how to debug it)

Hooks fail silently. When a hook doesn’t fire, there’s no error message. Your commit just goes through without review. Three things to check.

Hook doesn’t fire at all. Check the event name. git:pre-commit is not git:precommit or git:pre_commit. The colon and hyphen matter. Check that the file is in .claude/hooks/ with a .json extension.

Hook fires but no skill activates. The error Hook fired, but no skill matched means the skill path is wrong. The action.skill field is relative to .claude/skills/. If you reorganized skills into folders, include the folder: 09-communication/code-review not just code-review.

Hook runs too slowly. If a blocking hook takes more than 10 seconds, your workflow drags. Two fixes: make it non-blocking ("blocking": false) or narrow the pattern. **/*.ts reviews every TypeScript file. src/api/**/*.ts only reviews API files.

Pattern too broad. "pattern": "**/*.ts" reviews every TypeScript file including generated code, vendor files, and type declarations. Narrow it: "pattern": "src/**/*.ts" reviews only your source code. This also speeds up the hook.

# Quick diagnostic: check both directories exist and have files
ls -la .claude/hooks/*.json .claude/skills/*.md

What other hooks are worth building?

Two more that run without your intervention.

Auto-test on file save:

{
  "name": "auto-test",
  "event": "file:save",
  "pattern": "src/**/*.ts",
  "action": {
    "type": "skill",
    "skill": "testing",
    "prompt": "Find the test file for the saved source file. Run it. Report failures only."
  },
  "options": {
    "silent": true,
    "blocking": false
  }
}

Non-blocking so your editor stays responsive. Silent so you only hear from it when a test breaks.

Documentation sync on commit:

{
  "name": "doc-sync",
  "event": "git:pre-commit",
  "pattern": "src/**/*.ts",
  "action": {
    "type": "skill",
    "skill": "documentation",
    "prompt": "Compare exported functions in changed files with their JSDoc. Flag any function where the signature changed but docs didn't."
  },
  "options": {
    "silent": true,
    "blocking": false
  }
}

Catches stale documentation at the source. In six weeks this caught eight cases where a function signature changed but the JSDoc still showed the old parameters. Each one would have confused someone reading the code later.

When do you use blocking vs. non-blocking?

SettingUse whenTrade-off
blocking: true, silent: truePre-commit review5-10s delay per commit, catches bugs before history
blocking: false, silent: trueAuto-test on saveNo delay, background feedback on failures only
blocking: true, silent: falseMigration reviewBlocking because you need to see the result before committing
blocking: false, silent: falseDoc-sync, lintingBackground, shows all results in terminal feed

Start with blocking + silent for pre-commit. Add non-blocking hooks after you trust the system. Three hooks total covers the full cycle.

For the full list of hook event types, see Anthropic’s hooks guide. For 13 working hook implementations you can study and adapt, see disler/claude-code-hooks-mastery on GitHub (3.4k stars). It covers all 13 hook lifecycle events including PreToolUse, PostToolUse, Notification, Stop, SubagentStop, PreCompact, SessionStart, SessionEnd, and more.

What should you actually do?

  • If you have no hooks: build the pre-commit review hook from this article. Two files, 5 minutes. Claude catches bugs before they enter your git history.
  • If your hook fires but seems to miss things: narrow the review prompt. “Focus on bugs and security only” produces better results than “review everything.” Specific prompts get specific output.
  • If you want the full stack: add the auto-test hook next. Pre-commit catches problems at commit time. Auto-test catches them at save time. Together they form a feedback loop that runs while you work.

bottom_line

  • One pre-commit hook with a code review skill caught 14 bugs in 2 months, including one SQL injection. Setup time: 5 minutes. Cost per commit: less than a penny.
  • Hooks fail silently. When something stops working, check event names, skill paths, and file patterns first. The diagnostic takes 30 seconds.
  • Start with one blocking hook on pre-commit. Add non-blocking hooks on file save after you trust the system. Three hooks cover the full development cycle.

Frequently Asked Questions

What's the difference between Claude Code hooks and git hooks?+

Git hooks fire on git events only. Claude Code hooks fire on file changes, terminal commands, session events, and git events. Claude hooks trigger skills with AI-powered review. Git hooks run shell scripts.

How much do Claude Code hooks cost per commit?+

A pre-commit review of 50-100 lines consumes roughly 500-1,000 input tokens and 200-500 output tokens. At current pricing, that's about $0.005-$0.01 per commit. Twenty commits a day costs $0.10-$0.20.

Can Claude Code hooks block a commit?+

With blocking: true, the hook runs before the commit completes. Claude flags issues in your terminal. By default it doesn't prevent the commit, but you see the warnings before pushing.