The Problem
You want AI to run tests before committing code. You could write a rule in CLAUDE.md: "Always run tests before committing." Or you could write a hook that executes npm test automatically. The rule is ignored 50% of the time. The hook works but slows down every AI interaction. Neither approach feels right.
The issue: Rules are guidance that AI may or may not follow. Hooks are code that AI must execute. Choosing wrong means either unreliable enforcement (rules) or unnecessary overhead (hooks).
Different scenarios need different approaches. Sometimes you want AI to have flexibility (rules). Sometimes you need guaranteed execution (hooks). Most projects need both, strategically combined.
The Core Insight
Use rules for guidance and best practices. Use hooks for enforcement and automation. Rules describe "how to think," hooks describe "what to do."
Think of rules as advisory (like speed limit signs) and hooks as enforcement (like speed bumps). Speed limits work when drivers are paying attention. Speed bumps work always, but you don't put them everywhere because they slow everyone down.
Rules: Static Guidance
What Are Rules?
Rules are markdown files (.clinerules, CLAUDE.md, .cursorrules) that AI reads for context. They're declarative instructions.
# .clinerules
## TypeScript Rules
- Use `interface` over `type` for object shapes
- No `any` types - use `unknown` if type is truly unknown
- Prefer `const` over `let`, never use `var`
- Use optional chaining (`?.`) and nullish coalescing (`??`)
## Testing Rules
- Write tests for all business logic
- Aim for 80%+ code coverage
- Use descriptive test names
- Test behavior, not implementation
AI reads this and tries to follow it, but there's no guarantee. If you ask for code that violates a rule, AI might prioritize your direct instruction over the rule.
When to Use Rules
Use rules when:
- Context matters: "In this codebase, we prefer X over Y"
- Flexibility is needed: "Usually do this, but exceptions are okay"
- Teaching patterns: "Here's how we structure components"
- No side effects: Just guiding code generation, not executing anything
Good Rule Examples
# Guidance, not enforcement:
"Prefer Server Components in Next.js App Router. Only use 'use client' when you need:
- Browser APIs (window, localStorage)
- Event handlers (onClick, onChange)
- React hooks (useState, useEffect)"
"When creating API endpoints:
- Use Zod for request validation
- Return consistent error shapes: { error: string, details?: any }
- Always handle errors with try/catch"
"For database queries:
- Use Prisma (don't write raw SQL)
- Include error handling
- Use transactions for multi-step operations"
Hooks: Executable Automation
What Are Hooks?
Hooks are scripts that AI executes at specific lifecycle points. They run code, enforce policies, validate output.
# .clinerules with hooks
## Pre-Commit Hook
Before committing code, run:
```bash
#!/bin/bash
npm run lint || exit 1
npm run typecheck || exit 1
npm test || exit 1
```
## Post-Generate Hook
After generating code, run:
```bash
#!/bin/bash
# Check for common mistakes
grep -r "console.log" src/ && echo "Warning: console.log found"
grep -r "any" src/**/*.ts && echo "Warning: 'any' type found"
```
AI executes these scripts. If they fail, AI knows something is wrong and can fix it.
When to Use Hooks
Use hooks when:
- Enforcement required: "Must run tests before commit"
- Automated checks: "Validate all generated code"
- Side effects needed: "Generate types from schema"
- Integration required: "Update database schema"
Good Hook Examples
# Enforcement and automation:
## Pre-Commit Hook
```bash
#!/bin/bash
# Ensure code quality before commit
npm run lint --fix
npm run format
npm run typecheck
npm test
# Exit with error if any command failed
if [ $? -ne 0 ]; then
echo "Pre-commit checks failed"
exit 1
fi
```
## Code Generation Hook
```bash
#!/bin/bash
# Regenerate Prisma Client after schema changes
if git diff --cached --name-only | grep -q "prisma/schema.prisma"; then
npx prisma generate
echo "Prisma Client regenerated"
fi
```
## Type Safety Hook
```bash
#!/bin/bash
# Fail if any 'any' types in new code
git diff --cached --diff-filter=A --name-only | \
grep "\.ts$" | \
xargs grep -l ":\s*any" && \
echo "Error: New code contains 'any' types" && \
exit 1
```
Combining Rules and Hooks
Most effective setups use both strategically:
| Scenario | Rule | Hook |
|---|---|---|
| Code Style | "Use Prettier formatting" | Run prettier --write on save |
| Type Safety | "Avoid 'any' types" | Fail CI if 'any' found in diff |
| Testing | "Write tests for business logic" | Run tests pre-commit |
| Dependencies | "Prefer X library over Y" | Warn if Y is installed |
| Database | "Use Prisma for queries" | Generate Prisma Client after schema change |
Example: Full Setup
# .clinerules
## TypeScript Rules (Guidance)
- Use `unknown` instead of `any` for type safety
- Prefer `interface` over `type` for objects
- Use strict mode features (optional chaining, nullish coalescing)
## Hooks (Enforcement)
### Pre-Commit
```bash
#!/bin/bash
set -e
echo "Running pre-commit checks..."
# Format code
npm run format
# Lint and fix
npm run lint --fix
# Type check
npm run typecheck
# Run tests
npm test --passWithNoTests
echo "Pre-commit checks passed!"
```
### On Schema Change
```bash
#!/bin/bash
# Triggered when prisma/schema.prisma changes
echo "Prisma schema changed, regenerating client..."
npx prisma generate
echo "Running migrations in dev..."
npx prisma migrate dev
echo "Schema update complete!"
```
### Post-Generate Validation
```bash
#!/bin/bash
# Run after AI generates code
echo "Validating generated code..."
# Check for 'any' types
if grep -r ": any" src/**/*.ts 2>/dev/null; then
echo "⚠️ Warning: 'any' types found in generated code"
fi
# Check for console.log (should use logger)
if grep -r "console\\.log" src/ 2>/dev/null; then
echo "⚠️ Warning: console.log found (use logger instead)"
fi
# Check for TODOs
if grep -r "TODO" src/ 2>/dev/null; then
echo "ℹ️ Info: TODOs found in generated code"
fi
echo "Validation complete!"
```
Performance Considerations
Rules Are Free
Rules are just text. AI reads them once, they add no runtime cost. Use liberally.
Hooks Have Cost
Every hook executes, taking time. Balance enforcement vs speed:
| Hook Type | When to Run | Performance Impact |
|---|---|---|
| Linting | Pre-commit | Low (1-2 seconds) |
| Type checking | Pre-commit | Medium (5-10 seconds) |
| Full test suite | Pre-push (not pre-commit) | High (30+ seconds) |
| Build | CI only | Very high (minutes) |
The Slow Hook Problem
If hooks are too slow, developers (and AI) will skip them. Keep pre-commit hooks under 10 seconds. Move expensive checks to CI.
Smart Hook Design
#!/bin/bash
# Only run expensive checks on changed files
CHANGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep "\.ts$")
if [ -z "$CHANGED_FILES" ]; then
echo "No TypeScript files changed, skipping type check"
exit 0
fi
# Only type-check changed files
echo "$CHANGED_FILES" | xargs npx tsc --noEmit
# Only test affected files
npm test -- --findRelatedTests $CHANGED_FILES
Failure Patterns
1. Too Many Hooks
Symptom: AI takes 60 seconds to commit because hooks run a full test suite, build, and deploy preview.
Fix: Move slow checks to CI. Pre-commit should be < 10 seconds.
2. Rules Without Enforcement
Symptom: "Don't use 'any' types" rule exists, but generated code has them everywhere.
Fix: Add a hook that fails on 'any' types, or accept that rules are advisory.
3. Hooks That Always Fail
Symptom: Pre-commit hook fails on every attempt because test suite is broken.
Fix: Fix the tests or temporarily disable the hook (but set a reminder to re-enable).
The Escape Hatch
Always provide a way to skip hooks for emergencies: git commit --no-verify or environment variable: SKIP_HOOKS=1 git commit. Document when skipping is acceptable.
Quick Reference
Use Rules For:
- Coding conventions and style preferences
- Architectural patterns to follow
- Library choices and usage patterns
- Best practices and anti-patterns
- Context about "how we do things here"
Use Hooks For:
- Running linters and formatters
- Type checking
- Running tests
- Generating code (Prisma, GraphQL schemas)
- Enforcing code quality gates
- Updating dependencies after changes
Decision Tree:
Q: Does this need to happen EVERY time?
YES → Hook
NO → Rule
Q: Can AI decide when to apply this?
YES → Rule
NO → Hook
Q: Does this run code?
YES → Hook
NO → Rule
Q: Will this take > 10 seconds?
YES → CI check, not pre-commit hook
NO → Can be a hook
Hook Performance Targets:
- Pre-commit: < 10 seconds
- Pre-push: < 30 seconds
- CI: No limit (but faster is better)
Example .clinerules Structure:
# .clinerules
## Project Context (Rules)
[Stack, conventions, patterns]
## Pre-Commit Hook
```bash
npm run lint --fix
npm run typecheck
npm test --changed
```
## Post-Generate Hook
```bash
npm run generate-types
```
## Validation Hook
```bash
./scripts/check-code-quality.sh
```