DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Claude Code hooks: automating code review before every commit

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')"
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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('\n');
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}\`));
}
Enter fullscreen mode Exit fullscreen mode

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')"
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Lint check after edits:

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

Session context on startup:

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

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)
Enter fullscreen mode Exit fullscreen mode

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
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Auto-test after implementation:

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

Track file changes:

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

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.

Top comments (0)