Claude Code Hooks as Automated Quality Gates

James Phoenix
James Phoenix

Summary

Manual verification of AI-generated code is time-consuming and error-prone. Claude Code hooks automatically run linters, type checkers, and tests on every tool call, catching errors instantly. With keyboard shortcuts like Ctrl+O for diagnosing failures, you get immediate feedback loops that make AI-assisted development feel like having a real-time code reviewer watching your back.

The Problem

When working with AI-generated code, you face a tedious verification cycle:

  1. Claude Code writes a file
  2. You manually run npm run lint
  3. Find 5 linting errors
  4. Ask Claude to fix them
  5. Manually run tsc --noEmit
  6. Find 3 type errors
  7. Ask Claude to fix those
  8. Manually run npm test
  9. Find 2 test failures
  10. Repeat…

This manual verification loop is:

  • Time-consuming: Each check takes 30-60 seconds
  • Error-prone: Easy to forget a step
  • Frustrating: Errors discovered too late
  • Inefficient: Claude could fix issues immediately if it knew about them

The Solution

Claude Code Hooks automate quality checks by running them automatically on every tool call:

  • Pre-commit hooks: Run linters before code is written
  • Post-edit hooks: Run type checkers after editing files
  • Post-write hooks: Run tests after writing new code

When a hook fails, press Ctrl+O to instantly see the failure details and let Claude fix the issue immediately.

This creates a real-time feedback loop where Claude knows about errors within seconds and can fix them before you even notice.

Implementation

Directory Structure

Claude Code looks for hooks in .claude/hooks/:

project-root/
├── .claude/
│   └── hooks/
│       ├── pre-commit.json
│       ├── post-edit.json
│       └── post-write.json
├── package.json
└── src/

Hook Configuration Format

Each hook is a JSON file defining a command to run:

{
  "command": "command to execute",
  "description": "What this hook does",
  "continueOnError": false
}

Fields:

  • command: Shell command to run (supports placeholders like {file})
  • description: Human-readable explanation
  • continueOnError: If true, don’t block on failures (default: false)

Example 1: Linting Hook

Run ESLint on every file write:

// .claude/hooks/post-write.json
{
  "command": "npx eslint {file} --fix",
  "description": "Lint and auto-fix code style issues",
  "continueOnError": false
}

How it works:

  1. Claude writes a file (e.g., src/utils/auth.ts)
  2. Hook runs: npx eslint src/utils/auth.ts --fix
  3. If linting fails, Claude sees the error immediately
  4. Press Ctrl+O to view the failure
  5. Claude fixes the issues and rewrites the file

Example 2: Type Checking Hook

Run TypeScript type checker on edits:

// .claude/hooks/post-edit.json
{
  "command": "tsc --noEmit",
  "description": "Type check the entire project",
  "continueOnError": false
}

How it works:

  1. Claude edits a file to add a new function
  2. Hook runs: tsc --noEmit
  3. Type errors are caught immediately
  4. Claude sees: Error: Property 'email' does not exist on type 'User'
  5. Claude fixes the type error without manual intervention

Example 3: Test Execution Hook

Run related tests after writing code:

// .claude/hooks/post-write.json
{
  "command": "npm test -- --related {file} --passWithNoTests",
  "description": "Run tests related to the changed file",
  "continueOnError": false
}

How it works:

  1. Claude writes src/services/user-service.ts
  2. Hook runs: npm test -- --related src/services/user-service.ts
  3. Related test file user-service.test.ts executes
  4. If tests fail, Claude sees the assertion errors
  5. Claude fixes the implementation or updates the tests

Advanced Hook Patterns

Chaining Multiple Checks

Run multiple quality gates in sequence:

// .claude/hooks/post-write.json
{
  "command": "npx eslint {file} --fix && tsc --noEmit && npm test -- --related {file} --passWithNoTests",
  "description": "Lint, type check, and test in one pass",
  "continueOnError": false
}

Why this works:

  • && ensures each step must pass before the next runs
  • First failure stops execution and reports the error
  • Claude sees exactly which quality gate failed

Conditional Hooks by File Type

Run different checks for different file types:

#!/bin/bash
# .claude/hooks/post-write.sh

FILE=$1

if <a href="/posts/file-ts/">$FILE == *.ts</a>; then
  npx eslint "$FILE" --fix && tsc --noEmit
elif <a href="/posts/file-json/">$FILE == *.json</a>; then
  npx prettier "$FILE" --write
elif <a href="/posts/file/">$FILE == *.md</a>; then
  npx markdownlint "$FILE" --fix
fi

Then reference it in the hook:

{
  "command": ".claude/hooks/post-write.sh {file}",
  "description": "Run file-type-specific quality checks"
}

Performance Optimization: Scope Limitation

For large projects, full type checking on every edit is slow. Limit scope:

// .claude/hooks/post-edit.json
{
  "command": "tsc --noEmit --incremental",
  "description": "Incremental type checking (faster)",
  "continueOnError": false
}

Or only check the edited file and its imports:

{
  "command": "tsc --noEmit {file}",
  "description": "Type check only the edited file",
  "continueOnError": false
}

Keyboard Shortcuts

Ctrl+O: Diagnose Hook Failures

When a hook fails, press Ctrl+O to:

  • View the complete error output
  • See which hook failed (pre-commit, post-edit, post-write)
  • Get the exact command that was run
  • See stack traces and assertion failures

Example workflow:

Udemy Bestseller

Learn Prompt Engineering

My O'Reilly book adapted for hands-on learning. Build production-ready prompts with practical exercises.

4.5/5 rating
306,000+ learners
View Course
1. Claude writes code
2. Post-write hook runs: npm test -- --related src/auth.ts
3. Test fails with "Expected 200, got 401"
4. You see: "⚠️ Hook failed: post-write"
5. Press Ctrl+O
6. See full error:
   
   FAIL src/auth.test.ts
   ● authenticateUser › should return 200 for valid credentials
     expect(received).toBe(expected)
     Expected: 200
     Received: 401
     
7. Claude reads this, identifies the issue (wrong status code)
8. Claude fixes auth.ts to return 200 for valid credentials
9. Hook re-runs automatically, passes ✅

Best Practices

1. Start with Linting, Add More Later

Don’t configure all hooks at once. Start simple:

Week 1: Add linting only

// .claude/hooks/post-write.json
{"command": "npx eslint {file} --fix"}

Week 2: Add type checking

// .claude/hooks/post-edit.json
{"command": "tsc --noEmit"}

Week 3: Add testing

// .claude/hooks/post-write.json
{"command": "npx eslint {file} --fix && npm test -- --related {file}"}

This gradual adoption prevents overwhelming yourself with errors.

2. Use --fix Flags When Available

Automatic fixes reduce noise:

// ✅ Good: Auto-fix linting issues
{"command": "npx eslint {file} --fix"}

// ❌ Bad: Just report issues without fixing
{"command": "npx eslint {file}"}

ESLint’s --fix automatically corrects:

  • Missing semicolons
  • Incorrect indentation
  • Unused imports
  • Trailing commas

Claude sees fewer errors because many are auto-fixed.

3. Set Reasonable Timeouts

For slow checks, increase timeout or use continueOnError:

// For slow test suites
{
  "command": "npm test -- --related {file} --maxWorkers=2",
  "description": "Run tests with limited workers (faster)",
  "continueOnError": false
}

4. Use Incremental Checking

TypeScript’s --incremental flag caches results:

{
  "command": "tsc --noEmit --incremental",
  "description": "Incremental type checking (uses cache)"
}

Performance comparison:

  • Without --incremental: 8-12 seconds
  • With --incremental: 2-3 seconds (after first run)

5. Combine with CI/CD

Hooks catch errors locally, CI/CD catches what hooks miss:

# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - run: npm ci
      - run: npm run lint
      - run: npm run type-check
      - run: npm test

Why both?

  • Hooks: Fast feedback during development (2-5 seconds)
  • CI/CD: Comprehensive checks before merge (full test suite, integration tests)

Real-World Example

Scenario: Building a User Registration API

You’re building a user registration endpoint with Claude Code.

Without hooks:

1. Ask Claude to write register endpoint
2. Claude writes src/api/register.ts
3. You manually run: npm run lint
   → 3 linting errors (missing semicolons)
4. Ask Claude to fix linting
5. Claude fixes
6. You manually run: tsc --noEmit
   → 2 type errors (missing User type import)
7. Ask Claude to fix types
8. Claude fixes
9. You manually run: npm test
   → 1 test failure (wrong status code)
10. Ask Claude to fix test
11. Claude fixes
12. Finally works!

Total time: 8-10 minutes, 6 manual commands

With hooks:

1. Ask Claude to write register endpoint
2. Claude writes src/api/register.ts
3. Post-write hook runs automatically:
   - Lints: ❌ 3 errors
4. Claude sees errors immediately, auto-fixes
5. Post-write hook re-runs:
   - Lints: ✅
   - Type checks: ❌ 2 errors
6. Claude sees errors, adds User import
7. Post-edit hook runs:
   - Type checks: ✅
8. Post-write hook runs tests:
   - Tests: ❌ 1 failure
9. Press Ctrl+O, see "Expected 201, got 200"
10. Claude fixes status code
11. Hook re-runs:
    - Tests: ✅
12. Done!

Total time: 2-3 minutes, 0 manual commands

Result: 60-70% time savings, zero manual intervention.

Hook Execution Timeline

Pre-Commit Hooks

Run before any file modification:

User: "Add authentication to the API"
  ↓
Claude plans to modify: src/api/routes.ts
  ↓
Pre-commit hook runs (e.g., check for TODOs)
  ↓
If passes: Claude proceeds with edit
If fails: Claude sees error, adjusts plan

Use cases:

  • Check for TODOs or FIXMEs before committing
  • Verify no console.log statements
  • Ensure no hardcoded secrets

Post-Edit Hooks

Run after editing an existing file:

Claude edits: src/services/user.ts
  ↓
File is modified
  ↓
Post-edit hook runs (e.g., tsc --noEmit)
  ↓
If passes: Continue
If fails: Claude sees error, fixes immediately

Use cases:

  • Type checking
  • Linting
  • Format checking

Post-Write Hooks

Run after creating or overwriting a file:

Claude writes: src/utils/hash.ts
  ↓
New file created
  ↓
Post-write hook runs (e.g., npm test -- --related)
  ↓
If passes: File is ready
If fails: Claude sees error, fixes code or tests

Use cases:

  • Running related tests
  • Linting new files
  • Building/bundling

Troubleshooting

Hook Fails But No Error Shown

Press Ctrl+O to view the full error output. The summary might be truncated.

Hook Runs Too Slowly

Problem: Type checking takes 15 seconds on every edit.

Solutions:

  1. Use --incremental flag:
    {"command": "tsc --noEmit --incremental"}
    
  2. Limit scope to edited file:
    {"command": "tsc --noEmit {file}"}
    
  3. Use continueOnError for slow checks:
    {"command": "npm test", "continueOnError": true}
    

Hook Fails on Valid Code

Problem: Linter complains about intentional code patterns.

Solutions:

  1. Update linter config (.eslintrc.json):
    {
      "rules": {
        "no-console": "off"
      }
    }
    
  2. Use inline disables sparingly:
    // eslint-disable-next-line no-console
    console.log("Debug info");
    

Hooks Don’t Run

Problem: You configured hooks, but they’re not executing.

Checklist:

  1. ✅ Hooks are in .claude/hooks/ directory
  2. ✅ Files are named correctly (post-write.json, not postwrite.json)
  3. ✅ JSON is valid (run cat .claude/hooks/post-write.json | jq)
  4. ✅ Command exists (run manually: npx eslint --version)
  5. ✅ Claude Code is restarted after adding hooks

Metrics for Success

Time to Error Detection

Without hooks:

  • Write code → Manually test → Find error → Fix
  • Average: 5-10 minutes per error

With hooks:

  • Write code → Hook fails immediately → Fix
  • Average: 30-60 seconds per error

Error Detection Rate

Track how many errors are caught by hooks vs. CI/CD:

Errors caught by hooks: 85%
Errors caught by CI/CD: 12%
Errors caught in production: 3%

Goal: Hooks should catch 80%+ of errors before CI/CD runs.

Developer Satisfaction

Survey your team:

  • How often do you press Ctrl+O? (Higher = hooks working)
  • Do hooks slow you down? (Should be “No”)
  • Do hooks catch errors early? (Should be “Yes”)

Conclusion

Claude Code hooks transform AI-assisted development from a manual verification loop into an automated quality gate system. By running linters, type checkers, and tests automatically on every tool call, you catch errors within seconds instead of minutes.

Key Takeaways:

  1. Configure hooks gradually (linting → type checking → tests)
  2. Use --fix flags to auto-correct issues
  3. Press Ctrl+O to diagnose failures instantly
  4. Combine with CI/CD for comprehensive coverage
  5. Optimize for speed with incremental checks

The result: A development workflow where Claude knows about errors immediately and fixes them before you even notice. It’s like having a real-time code reviewer watching your back.

Related Concepts

References

Topics
AutomationCi CdClaude CodeHooksLintingQuality GatesTestingType CheckingWorkflow Optimization

More Insights

Cover Image for Thought Leaders

Thought Leaders

People to follow for compound engineering, context engineering, and AI agent development.

James Phoenix
James Phoenix
Cover Image for Systems Thinking & Observability

Systems Thinking & Observability

Software should be treated as a measurable dynamical system, not as a collection of features.

James Phoenix
James Phoenix