◉CLAUDE LABJP
●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…
Articles/Claude Code
⟐ Claude Code/2026-05-05Advanced

Building a Zero-Touch Code Review Environment with Claude Code Hooks

Learn how to use Claude Code's hook system to automatically review code on every tool execution. Covers PostToolUse, Stop hooks, and the pitfalls to avoid when implementing PreToolUse blockers.

Claude Code264Hooks6Automation45Code ReviewDeveloper Tools

There's a quiet disappointment in opening a PR and seeing nothing but "LGTM" from a reviewer. In a busy development team, giving every commit the attention it deserves isn't always realistic. To close that gap, I built a setup using Claude Code's hook system that automatically reviews code as I work — no manual triggering required.

What surprised me most was that hooks aren't just a "run a command" feature. By combining PreToolUse, PostToolUse, and Stop events thoughtfully, you create an interactive quality layer that runs alongside Claude Code itself — distinct from CI/CD, and far more immediate.

Understanding When Hooks Fire

Hooks are defined in .claude/settings.json. There are four trigger points:

  • PreToolUse: Before a tool runs (returning exit 2 blocks the tool)
  • PostToolUse: After a tool completes (use this for analysis and reporting)
  • Notification: When Claude sends a notification
  • Stop: When the session ends

For automatic code review, PostToolUse on the Bash tool is the right choice. Every time Claude Code runs a shell command, the hook fires and checks whether any tracked files changed.

One thing to get right early: use matcher to target specific tools. Attaching heavy processes to every tool invocation makes Claude Code noticeably sluggish.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python3 ~/.claude/hooks/auto-review.py"
          }
        ]
      }
    ]
  }
}

The Review Script

Claude Code passes context to hooks via stdin as JSON. Here's the script that reads that context, detects changed files, and asks Claude to review them:

#!/usr/bin/env python3
import json
import sys
import subprocess
 
def get_changed_files():
    result = subprocess.run(
        ["git", "diff", "--name-only", "HEAD"],
        capture_output=True, text=True
    )
    files = result.stdout.strip().split("\n")
    return [f for f in files if f.endswith((".ts", ".tsx", ".py", ".go"))]
 
def review_with_claude(code: str, file_path: str):
    prompt = f"""Review the following code briefly.
If there are issues, describe them. If everything looks fine, just respond with "OK".
 
File: {file_path}

{code[:2000]}

 
    result = subprocess.run(
        ["claude", "-p", prompt, "--output-format", "text"],
        capture_output=True, text=True, timeout=30
    )
    if result.returncode == 0:
        print(result.stdout)
 
def main():
    try:
        hook_input = json.loads(sys.stdin.read())
    except json.JSONDecodeError:
        return
 
    if hook_input.get("tool_name") != "Bash":
        return
 
    changed_files = get_changed_files()
    if not changed_files:
        return
 
    for file_path in changed_files[:3]:
        try:
            with open(file_path) as f:
                code = f.read()
        except (FileNotFoundError, PermissionError):
            continue
 
        print(f"\n📋 Auto-review: {file_path}")
        review_with_claude(code, file_path)
 
if __name__ == "__main__":
    main()

In practice, the review comments appear inline in the terminal as Claude Code works. It felt noisy at first, but after a few sessions it became useful — catching loose variable names and missing error handling that I would have deferred to "later."

Generating Session Reports with Stop Hooks

The Stop hook runs when a Claude Code session ends, which makes it ideal for summarizing what happened.

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "python3 ~/.claude/hooks/session-report.py"
          }
        ]
      }
    ]
  }
}
#!/usr/bin/env python3
import subprocess
from datetime import datetime
from pathlib import Path
 
def get_recent_commits():
    result = subprocess.run(
        ["git", "log", "--since=6 hours ago", "--oneline", "--no-merges"],
        capture_output=True, text=True
    )
    return result.stdout.strip()
 
def generate_summary(commits: str) -> str:
    if not commits:
        return "No commits in this session."
 
    prompt = f"""Summarize the following git commits in 3 bullet points.
Use plain language that's easy to share in a standup.
 
Commits:
{commits}"""
 
    result = subprocess.run(
        ["claude", "-p", prompt, "--output-format", "text"],
        capture_output=True, text=True, timeout=30
    )
    return result.stdout if result.returncode == 0 else "Summary generation failed."
 
def main():
    commits = get_recent_commits()
    summary = generate_summary(commits)
 
    log_dir = Path.home() / "work-logs"
    log_dir.mkdir(exist_ok=True)
 
    today = datetime.now().strftime("%Y-%m-%d")
    log_file = log_dir / f"{today}.md"
 
    with open(log_file, "a") as f:
        timestamp = datetime.now().strftime("%H:%M")
        f.write(f"\n## {timestamp} Session ended\n\n{summary}\n")
 
    print(f"📝 Work log saved: {log_file}")
 
if __name__ == "__main__":
    main()

These logs become genuinely useful when writing weekly summaries or project status updates — things that normally require reconstructing history from memory.

The Gotcha with PreToolUse Blockers

PreToolUse hooks that return exit 2 can block tool execution entirely. This sounds powerful, but there's a failure mode worth knowing: if your blocker is too broad, Claude Code enters a loop where it cannot recover.

I learned this when I built a hook to block Bash execution whenever TypeScript had compilation errors. The problem: Claude needs to run Bash commands to fix those errors. Block the tool, block the fix, block the tool again.

The design principle that works: never block the tools Claude needs to self-correct. Use precise matcher patterns and limit blocking to genuinely dangerous commands:

#!/usr/bin/env python3
import json
import sys
import re
 
DANGEROUS_PATTERNS = [
    r"rm\s+-rf\s+/",
    r"DROP\s+TABLE",
]
 
def main():
    try:
        hook_input = json.loads(sys.stdin.read())
    except json.JSONDecodeError:
        sys.exit(0)
 
    command = hook_input.get("tool_input", {}).get("command", "")
 
    for pattern in DANGEROUS_PATTERNS:
        if re.search(pattern, command, re.IGNORECASE):
            print(f"Blocked dangerous command: {command[:100]}")
            sys.exit(2)
 
if __name__ == "__main__":
    main()

When exit 2 fires, Claude Code receives your stdout message and uses it to decide what to do next. A descriptive message helps Claude take a sensible alternative action rather than retrying the same blocked command.

Scope: User vs. Project

Hooks can live in two places:

  • ~/.claude/settings.json — applies to all projects
  • .claude/settings.json — applies to the current project only

The review and reporting hooks covered here are generic enough for the user scope. Project-specific blockers (say, one that enforces your team's linting rules) belong in the project scope so they don't interfere with other work.

Setup is minimal:

mkdir -p ~/.claude/hooks
chmod +x ~/.claude/hooks/auto-review.py
chmod +x ~/.claude/hooks/session-report.py

Start with the Stop hook and the session report. It has no risk — it only runs after Claude Code finishes — and you'll immediately have a habit of ending sessions with a written summary rather than a blank memory.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⟐ Claude Code2026-04-27
Claude Code Hooks: A Field Guide to the 8 Core Hooks — and the 33-Event Catalog They Now Live In
Claude Code hooks are powerful, but most people give up before figuring out which event does what. Here's the field guide I wish I had when I started — six months of running hooks in production, distilled.
⟐ Claude Code2026-09-01
Drop the quotes on a heredoc and the prices in your log quietly change
An unquoted heredoc runs the variables and backticks inside its body. Here are the four ways my log got rewritten, how the three quoting forms compare, a safe placeholder-and-sed pattern, and a small script for auditing what you already have.
⟐ Claude Code2026-08-22
Handing a long job to another session — and the completion marker for when the notification never arrives
How to use notify_when_idle to hear when another Claude Code session finishes, and a small completion marker that keeps you from waiting forever when the notification is dropped.