Let’s get something out of the way: your CLAUDE.md file isn’t enforcing anything.
Neither is .cursorrules, .github/copilot-instructions.md, or any other instruction file. These are context, not constraints. The LLM receives them as user-provided guidance and may disregard them at will. The model’s training, policy layers, and system prompt take precedence.
If you want actual enforcement, you need to build it yourself.
The Enforcement Hierarchy
Here’s what actually works, ranked by enforceability:
| Layer | Enforcement Level | Bypassable? |
|---|---|---|
| Git server controls | Hard | No (admin only) |
| CI/CD required checks | Hard | No (if branch protected) |
| Agent hooks (Claude Code, Kiro) | Hard (local) | Yes (user can disable) |
| Pre-commit/pre-push hooks | Soft | Yes (--no-verify) |
| Rule files (.cursorrules, etc.) | Advisory | Yes (LLM discretion) |
The design principle is simple: security gates at CI/server level, convenience at local level, rule files for guidance only.
Tool-Specific Reality Check
Not all AI coding tools are created equal. Here’s what each one actually supports (as of late 2025):
Claude Code
-
Instruction file:
CLAUDE.md - Hook system: Yes—10 lifecycle events that can block actions
- Can actually enforce: Yes, via PreToolUse hooks
Claude Code is currently the most enforceable AI coding tool. Its hook system allows you to run scripts before file writes, bash commands, or other actions—and block them if they violate your rules.
Cursor
-
Instruction file:
.cursor/rules/*.mdc - Hook system: No
- Can actually enforce: External only (git hooks, CI)
GitHub Copilot
-
Instruction file:
.github/copilot-instructions.md(supports path scoping) - Hook system: No
- Can actually enforce: External only
Gemini CLI
-
Instruction file:
GEMINI.md - Hook system: No
- Can actually enforce: External only
Others (Amazon Q, Windsurf, Aider)
None of these have hook systems. They all rely on external enforcement.
Building a Real Enforcement Stack
Here’s how to actually enforce standards, using Claude Code as an example since it has the most capable hook system.
1. PreToolUse Hooks (Fast Feedback)
Block violations before they happen:
#!/usr/bin/env python3
# .claude/hooks/validate_write
import json
import sys
import re
input_data = json.load(sys.stdin)
file_path = input_data.get('tool_input', {}).get('file_path', '')
content = input_data.get('tool_input', {}).get('content', '')
errors = []
# Block test files in wrong location
if re.search(r'(^|/)spec/', file_path) or file_path.endswith('_spec.py'):
errors.append("Cannot create spec files. Use pytest in tests/.")
# Block banned packages
if re.search(r'from django', content):
errors.append("Cannot use Django. Use FastAPI.")
# Block wildcard imports
if re.search(r'from \w+ import \*', content):
errors.append("Cannot use wildcard imports.")
if errors:
print(f"BLOCKED: {'; '.join(errors)}", file=sys.stderr)
sys.exit(2)
Configure in .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [{
"type": "command",
"command": ".claude/hooks/validate_write",
"timeout": 10
}]
}
]
}
}
2. Git Hooks (Pre-Commit)
Catch anything that slips through:
Python project:
#!/bin/bash
# .git/hooks/pre-commit
# Check for banned packages
if grep -rE "^from django" . --include="*.py" 2>/dev/null; then
echo "ERROR: Django is banned. Use FastAPI."
exit 1
fi
# Lint staged files
STAGED_PY=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$')
if [[ -n "$STAGED_PY" ]]; then
echo "$STAGED_PY" | xargs ruff check
fi
TypeScript project:
#!/bin/bash
# .git/hooks/pre-commit
# Check for console.log in production code
if grep -rE "console\.log" src/ --include="*.ts" 2>/dev/null; then
echo "ERROR: Remove console.log statements."
exit 1
fi
# Lint and type-check staged files
STAGED_TS=$(git diff --cached --name-only --diff-filter=ACM | grep '\.tsx\?$')
if [[ -n "$STAGED_TS" ]]; then
npx eslint $STAGED_TS && npx tsc --noEmit
fi
Ruby project:
#!/bin/bash
# .git/hooks/pre-commit
# Check for Devise
if grep -rE "gem\s+['\"]devise['\"]" Gemfile 2>/dev/null; then
echo "ERROR: Devise is banned. Use Rails built-in auth."
exit 1
fi
# Check for RSpec files
if find . -path ./vendor -prune -o -name "*_spec.rb" -print | grep -q .; then
echo "ERROR: RSpec files detected. Use Minitest."
exit 1
fi
# Lint staged files
STAGED_RB=$(git diff --cached --name-only --diff-filter=ACM | grep '\.rb$')
if [[ -n "$STAGED_RB" ]]; then
echo "$STAGED_RB" | xargs bundle exec rubocop --force-exclusion
fi
3. Pre-Push Hook (Full Tests)
Run the full suite before pushing:
Python project:
#!/bin/bash
# .git/hooks/pre-push
echo "Running tests..." && pytest || exit 1
echo "Running linter..." && ruff check . || exit 1
echo "Type checking..." && mypy . || exit 1
TypeScript project:
#!/bin/bash
# .git/hooks/pre-push
echo "Running tests..." && npm test || exit 1
echo "Running linter..." && npx eslint . || exit 1
echo "Type checking..." && npx tsc --noEmit || exit 1
Ruby project:
#!/bin/bash
# .git/hooks/pre-push
echo "Running tests..." && rails test || exit 1
echo "Running linter..." && bundle exec rubocop || exit 1
4. CI/CD (The Real Gate)
This is where enforcement actually happens:
Python project:
# .github/workflows/enforce.yml
name: Enforcement
on: [push, pull_request]
jobs:
check-patterns:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check banned patterns
run: |
if grep -rE "^from django" . --include="*.py"; then
echo "::error::Django is banned"
exit 1
fi
test:
runs-on: ubuntu-latest
needs: check-patterns
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- run: pip install -r requirements.txt && pytest
lint:
runs-on: ubuntu-latest
needs: check-patterns
steps:
- uses: actions/checkout@v4
- run: pip install ruff mypy && ruff check . && mypy .
TypeScript project:
# .github/workflows/enforce.yml
name: Enforcement
on: [push, pull_request]
jobs:
check-patterns:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check banned patterns
run: |
if grep -rE ":\s*any\b" src/ --include="*.ts"; then
echo "::error::any type is banned"
exit 1
fi
test:
runs-on: ubuntu-latest
needs: check-patterns
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci && npm test
lint:
runs-on: ubuntu-latest
needs: check-patterns
steps:
- uses: actions/checkout@v4
- run: npm ci && npx eslint . && npx tsc --noEmit
Ruby project:
# .github/workflows/enforce.yml
name: Enforcement
on: [push, pull_request]
jobs:
check-patterns:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check banned patterns
run: |
if grep -rE "gem\s+['\"]devise['\"]" Gemfile; then
echo "::error::Devise is banned"
exit 1
fi
if find . -name "*_spec.rb" | grep -q .; then
echo "::error::RSpec files detected"
exit 1
fi
test:
runs-on: ubuntu-latest
needs: check-patterns
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
- run: bundle install && rails test
lint:
runs-on: ubuntu-latest
needs: check-patterns
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
- run: bundle install && bundle exec rubocop
5. Branch Protection (Non-Negotiable)
Configure in GitHub Settings → Branches:
- ✓ Require pull request before merging
- ✓ Require status checks to pass
- ✓ Require branches to be up to date
- ✓ Do not allow bypassing
This is the actual hard enforcement. Everything else is convenience.
The Staged Check Model
Don’t run your full test suite in agent hooks. It’s slow, surprising, and can timeout. Instead, use staged checks:
| Stage | What Runs | When | Blocking? |
|---|---|---|---|
| PreToolUse | Pattern checks | Before file write | Yes |
| PostToolUse | Format file, quick lint | After file write | No |
| Pre-commit | Pattern checks, lint staged | Before commit | Yes |
| Pre-push | Full tests, full lint | Before push | Yes |
| CI | Everything | On push/PR | Yes (hard) |
This gives fast feedback during development without surprise 60-second waits.
What About the Instruction Files?
They’re still useful! Just don’t rely on them for enforcement.
Use instruction files to:
- Explain architectural decisions
- Document patterns and conventions
- Provide context about the codebase
- Give examples of good code
Don’t use them to:
- Enforce security requirements
- Guarantee compliance with standards
- Block specific patterns or libraries
Think of them as onboarding docs for AI assistants. They help the AI write better code on the first try, reducing the back-and-forth. But the enforcement happens elsewhere.
The Bottom Line
Hard enforcement comes from repo/CI controls. Agent hooks add local deterministic gates for faster feedback, but they’re bypassable and shouldn’t be the only defense.
The enforcement stack:
- Branch protection + required CI → non-negotiable gate
- Pre-push hooks → catch issues before they hit CI
- Agent hooks → fast feedback during AI sessions
- Rule files → guidance for LLMs (advisory only)
Trust but verify. Actually, just verify.
Want to manage your engineering rules centrally and enforce them across all your AI coding tools? Try Convext →