Claude Code hooks: automating code review before every commit

Dev.to / 4/8/2026

💬 OpinionDeveloper Stack & InfrastructureTools & Practical Usage

Key Points

  • Claude Code “hooks” can run custom shell commands at specific workflow events such as PreToolUse (before Claude executes a tool), PostToolUse (after it completes), or UserPromptSubmit (when you send a message).
  • Hooks are configured via .claude/settings.json or a project-level CLAUDE.md, enabling project-scoped automation of code-assist behaviors.
  • The article’s key example shows a PreToolUse hook that triggers only when Claude is about to run a git commit, by using a condition that checks the tool input command.
  • The provided script performs an automated review by reading the staged diff (git diff --cached) and running a review routine before the commit proceeds.
  • This approach helps standardize lightweight code review and quality checks in the commit workflow without requiring manual steps each time.

Claude Code hooks let you run custom scripts at specific points in the workflow — before a tool runs, after a tool completes, or when the user submits a prompt. The most valuable hook: automated code review before every commit.

What hooks are

Hooks are shell commands that fire on events:

  • PreToolUse — before Claude runs a tool (Bash, Edit, Write, etc.)
  • PostToolUse — after a tool completes
  • UserPromptSubmit — when you send a message

You configure them in .claude/settings.json or project-level CLAUDE.md.

The pre-commit review hook

Here's a hook that runs a quick code review before every git commit:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "command": "node scripts/pre-commit-review.js",
        "condition": "toolInput.command.includes('git commit')"
      }
    ]
  }
}

The condition field ensures the hook only fires when Claude is about to run git commit, not every Bash command.

// scripts/pre-commit-review.js
const { execSync } = require('child_process');

// Get the staged diff
const diff = execSync('git diff --cached').toString();

if (diff.length === 0) {
  console.log('No staged changes');
  process.exit(0);
}

const checks = [];

// Check for console.log left in
if (diff.includes('console.log')) {
  checks.push('WARNING: console.log found in staged changes');
}

// Check for TODO/FIXME
const todoMatches = diff.match(/\+.*(?:TODO|FIXME|HACK|XXX)/gi);
if (todoMatches) {
  checks.push(\`WARNING: \${todoMatches.length} TODO/FIXME comments found\`);
}

// Check for .env or secrets
if (diff.includes('.env') || diff.includes('API_KEY') || diff.includes('SECRET')) {
  checks.push('CRITICAL: Possible secrets in staged changes');
}

// Check for large files
const stagedFiles = execSync('git diff --cached --name-only').toString().split('
');
for (const file of stagedFiles) {
  if (!file) continue;
  try {
    const size = execSync(\`wc -c < "\${file}"\`).toString().trim();
    if (parseInt(size) > 1000000) {
      checks.push(\`WARNING: Large file staged: \${file} (\${Math.round(parseInt(size)/1024)}KB)\`);
    }
  } catch {}
}

if (checks.length > 0) {
  console.log('Pre-commit review findings:');
  checks.forEach(c => console.log(\`  \${c}\`));
}

Other useful hooks

Auto-format on file write:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "command": "npx prettier --write $TOOL_FILE_PATH",
        "condition": "toolInput.file_path?.endsWith('.ts') || toolInput.file_path?.endsWith('.tsx')"
      }
    ]
  }
}

Lint check after edits:

{
  "PostToolUse": [
    {
      "matcher": "Edit",
      "command": "npx eslint $TOOL_FILE_PATH --quiet",
      "condition": "toolInput.file_path?.match(/\.(ts|tsx|js|jsx)$/)"
    }
  ]
}

Session context on startup:

{
  "UserPromptSubmit": [
    {
      "matcher": "*",
      "command": "echo 'Branch: '$(git branch --show-current)' | Last commit: '$(git log --oneline -1)",
      "condition": "true"
    }
  ]
}

The hook lifecycle

You type a message
  → UserPromptSubmit hooks fire
    → Claude decides to use a tool
      → PreToolUse hooks fire (can block the tool)
        → Tool executes
          → PostToolUse hooks fire (can add context)

PreToolUse hooks can return non-zero to block the tool. This is how you prevent Claude from running destructive commands or committing secrets.

Production hook patterns

Block dangerous commands:

{
  "PreToolUse": [
    {
      "matcher": "Bash",
      "command": "echo 'BLOCKED: destructive command'",
      "condition": "toolInput.command.match(/rm -rf|drop table|force push/i)",
      "blocking": true
    }
  ]
}

Auto-test after implementation:

{
  "PostToolUse": [
    {
      "matcher": "Write",
      "command": "npm test -- --passWithNoTests 2>/dev/null",
      "condition": "toolInput.file_path?.includes('/src/')"
    }
  ]
}

Track file changes:

{
  "PostToolUse": [
    {
      "matcher": "Edit|Write",
      "command": "echo $(date) $TOOL_FILE_PATH >> .claude/change-log.txt"
    }
  ]
}

Why hooks matter

Without hooks, Claude Code is reactive — it does what you ask. With hooks, it's proactive — it checks, validates, and guards automatically. The pre-commit review hook alone has caught secrets in staged files, oversized binaries, and leftover debug statements that would have shipped to production.

Hooks are the difference between "AI that writes code" and "AI that writes code responsibly."

The Ship Fast Skill Pack includes 5 production-tested hooks alongside 10 Claude Code skills. Pre-commit review, auto-formatting, lint gates, test runners, and session context — all configured and ready to use.