Hooks vs Rules: When to Use Each

Module 05: AI Project Configuration | Expansion Guide

Back to Module 05

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:

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:

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:

Use Hooks For:

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:

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
```