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:
- Claude Code writes a file
- You manually run
npm run lint - Find 5 linting errors
- Ask Claude to fix them
- Manually run
tsc --noEmit - Find 3 type errors
- Ask Claude to fix those
- Manually run
npm test - Find 2 test failures
- 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 explanationcontinueOnError: Iftrue, 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:
- Claude writes a file (e.g.,
src/utils/auth.ts) - Hook runs:
npx eslint src/utils/auth.ts --fix - If linting fails, Claude sees the error immediately
- Press Ctrl+O to view the failure
- 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:
- Claude edits a file to add a new function
- Hook runs:
tsc --noEmit - Type errors are caught immediately
- Claude sees:
Error: Property 'email' does not exist on type 'User' - 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:
- Claude writes
src/services/user-service.ts - Hook runs:
npm test -- --related src/services/user-service.ts - Related test file
user-service.test.tsexecutes - If tests fail, Claude sees the assertion errors
- 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:
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:
- Use
--incrementalflag:{"command": "tsc --noEmit --incremental"} - Limit scope to edited file:
{"command": "tsc --noEmit {file}"} - Use
continueOnErrorfor slow checks:{"command": "npm test", "continueOnError": true}
Hook Fails on Valid Code
Problem: Linter complains about intentional code patterns.
Solutions:
- Update linter config (
.eslintrc.json):{ "rules": { "no-console": "off" } } - 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:
- ✅ Hooks are in
.claude/hooks/directory - ✅ Files are named correctly (
post-write.json, notpostwrite.json) - ✅ JSON is valid (run
cat .claude/hooks/post-write.json | jq) - ✅ Command exists (run manually:
npx eslint --version) - ✅ 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:
- Configure hooks gradually (linting → type checking → tests)
- Use
--fixflags to auto-correct issues - Press Ctrl+O to diagnose failures instantly
- Combine with CI/CD for comprehensive coverage
- 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
- Test-Based Regression Patching – Write tests that make bugs illegal before fixing
- Quality Gates as Information Filters – How checks reduce invalid code states
- Verification Sandwich Pattern – Test → Implement → Verify workflow
- Early Linting Prevents Ratcheting – Catch issues before they compound
- Compounding Effects of Quality Gates – How stacked gates multiply quality improvements
- Building the Factory – Build infrastructure that builds infrastructure, including automated quality gate systems
- YOLO Mode Configuration – Use hooks as automated safety gates to enable permission-free development
References
- Claude Code Hooks Documentation – Official documentation for configuring Claude Code hooks
- ESLint Auto-Fix Rules – Learn which ESLint rules support automatic fixing
- TypeScript Incremental Compilation – How –incremental flag speeds up type checking

