guardrail
/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 |