/guardrail
Generate custom lint rules that enforce architectural decisions at edit time.
Philosophy
Lint rules are the highest-leverage codification target. They're cheaper than hooks (no custom Python), more durable than CLAUDE.md (automated, not advisory), and work in CI too (not just Claude Code). A lint rule catches violations the moment code is written — and fast-feedback.py surfaces the error immediately so Claude self-corrects.
When to Use
-
Import boundaries ("all DB access through repository layer")
-
API conventions ("routes must start with /api/v1")
-
Deprecated pattern blocking ("no direct fetch, use apiClient")
-
Auth enforcement ("handlers must call requireAuth")
-
Naming conventions that go beyond basic linting
Workflow
Phase 1: Accept Pattern
Parse the input. It can be:
-
Natural language: "all database queries must go through the repository layer"
-
Code example: "this import is wrong: import { db } from './db' "
-
Discovery mode: scan codebase for architectural invariants (when invoked by /tune-repo )
Clarify the constraint:
-
What EXACTLY should be flagged? (imports, function calls, patterns)
-
What's the fix? (alternative import, wrapper function)
-
Are there exceptions? (test files, migrations, the repository itself)
Phase 2: Choose Engine
Criterion ESLint ast-grep
Language JS/TS only Any (Python, Go, Rust, etc.)
Fixable Yes (auto-fix) Yes (rewrite)
Testing RuleTester built-in YAML snapshot tests
Config Flat config plugin sgconfig.yml
Speed Fast Very fast
Default: ESLint for JS/TS projects, ast-grep for everything else. If --engine is specified, use that.
Phase 3: Generate Rule
Read the reference docs for the chosen engine:
-
ESLint: references/eslint-rule-anatomy.md
-
ast-grep: references/ast-grep-rule-anatomy.md
Read the appropriate template:
- ESLint: templates/eslint-rule.js
- templates/eslint-rule-test.js
- ast-grep: templates/ast-grep-rule.yml
Generate:
-
Rule implementation with clear error message and fix suggestion
-
Rule metadata (docs URL, fixable, schema)
-
Test cases (valid AND invalid examples from the actual codebase)
Phase 4: Test
ESLint:
Run RuleTester
node guardrails/rules/<rule-name>.test.js
Or if project uses a test runner:
npx vitest run guardrails/rules/<rule-name>.test.js
ast-grep:
sg scan --config guardrails/sgconfig.yml --test
Also verify against the real codebase:
ESLint: run rule on entire project, expect 0 or known violations
npx eslint --no-warn-ignored --rule 'guardrails/<rule-name>: error' .
ast-grep: scan project
sg scan --config guardrails/sgconfig.yml
Phase 5: Install
Create the guardrails/ directory structure if it doesn't exist:
guardrails/ README.md # Catalog of all custom rules index.js # ESLint local plugin barrel (JS/TS projects) sgconfig.yml # ast-grep config (if non-JS rules exist) rules/ <rule-name>.js # ESLint rule implementation <rule-name>.test.js # ESLint RuleTester <rule-name>.yml # ast-grep rule
ESLint integration (flat config, zero npm dependencies):
// guardrails/index.js import noDirectDbImport from "./rules/no-direct-db-import.js";
export default { rules: { "no-direct-db-import": noDirectDbImport, }, };
// eslint.config.js — add to existing config import guardrails from "./guardrails/index.js";
export default [ // ... existing config { plugins: { guardrails }, rules: { "guardrails/no-direct-db-import": "error", }, }, ];
ast-grep integration:
guardrails/sgconfig.yml
ruleDirs:
- rules
Phase 6: Document
Update guardrails/README.md with:
<rule-name>
Engine: ESLint | ast-grep Pattern: <what it enforces> Rationale: <why — link ADR if exists> Auto-fix: yes | no Exceptions: <files/patterns excluded>
Output
GUARDRAIL CREATED:
- Rule: guardrails/rules/<name>.<ext>
- Test: guardrails/rules/<name>.test.<ext>
- Engine: ESLint | ast-grep
- Violations found: N (in current codebase)
- Auto-fixable: yes | no
- Config updated: eslint.config.js | guardrails/sgconfig.yml
Anti-Patterns
-
Rules that fire on >20% of files (too broad, probably wrong constraint)
-
Rules without tests (defeats the purpose)
-
Rules without clear error messages (Claude can't self-correct from "error")
-
Duplicating built-in ESLint/Ruff rules (check first)
-
Over-specific rules that match one file (use CLAUDE.md instead)
Integration
Consumed by How
fast-feedback.py
Runs eslint <file> and sg scan <file> on every edit
/codify-learning
Routes "lint rule" target here
/done
Routes "lint rule" target here
/tune-repo
Discovers patterns, recommends /guardrail invocations
/check-quality
Audits guardrails/ completeness
CI (GitHub Actions) Standard eslint . or sg scan in workflow