8 min read

LLM Rule Enforcement: What Actually Works

Convext
Convext Staff

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

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

GitHub Copilot

Gemini CLI

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:

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:

Don’t use them to:

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:

  1. Branch protection + required CI → non-negotiable gate
  2. Pre-push hooks → catch issues before they hit CI
  3. Agent hooks → fast feedback during AI sessions
  4. 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 →