/review-guards — turn review rules into guards
Goal: a review rule stated once should never depend on a reviewer's memory again. Mechanical rules become ast-grep guards (exit code decides); everything else becomes written guidance reviewers receive automatically.
1. Triage each rule
| Rule kind | Destination |
|---|---|
Structural / mechanical ("no console.log outside logger", "every handler calls auth middleware", "no raw SQL in controllers") |
ast-grep rule |
| Type-aware or flow-sensitive | existing linter / tsc config — do NOT emulate a type checker with patterns |
| Qualitative (naming taste, architecture judgement) | REVIEW_GUIDELINES.md in repo root — the /review extension and review subagents pick it up |
If a rule can't name a concrete AST shape, it is not an ast-grep rule.
2. Project setup (once)
sgconfig.yml # ruleDirs: [.ast-grep/rules], testConfigs: [{testDir: .ast-grep/tests}]
.ast-grep/rules/ # one YAML per rule
.ast-grep/tests/ # one test per rule: valid + invalid snippets
Requires ast-grep on PATH (brew package, already in chezmoi packages).
3. Write each rule TDD-style
- Write the test FIRST:
.ast-grep/tests/<id>-test.ymlwithvalid:(must not match) andinvalid:(must match) snippets — the invalid one is the exact code from the review finding that motivated the rule. - Write
.ast-grep/rules/<id>.yml:id,language,severity: error,message(say WHY, not just what),rule:(pattern/kind/inside…). ast-grep test --skip-snapshot-tests— must printRunning N testswith your test INCLUDED and pass. Pitfall: a test whose rule id does not exist is silently skipped (0 passed, exit 0) — the count is the proof the test ran. A wrong rule fails loudly (non-zero exit). Optionally pin snapshots withast-grep test -Uand drop the flag afterwards.ast-grep scanon the repo: every existing hit is either fixed now or the rule's scope is narrowed — never ship a guard the codebase already violates.
severity: error blocks (scan exits non-zero); warning reports only.
4. Guard candidates — what is worth codifying
Distilled from the ast-grep catalog, coderabbitai/ast-grep-essentials and semgrep-rules taxonomies. Offer these when the user asks "what guards should this project have?":
- Security (use the pack below first): hardcoded secrets (JWT/session/DB keys), weak crypto (DES/RC4/ECB, short RSA), cookies missing httponly/secure/samesite, JWT decode without verify, bind to 0.0.0.0, debug mode in prod code, insecure deserialization, XXE flags.
- Correctness traps:
awaitinsidePromise.all([...]), JSXcond && <X/>short-circuit with non-boolean, Godefer f()evaluating too early, empty catch blocks. (Flow/type-sensitive ones — floating promises — belong to the typed linter, not patterns.) - Project conventions: console vs logger (allow in catch), layer
boundaries ("no DB/fetch in handlers/components"), import hygiene (no
barrel imports, forbidden deep imports), deprecated internal APIs during
migrations (rule doubles as
ast-grep scan --rewritecodemod). - Agent/LLM code (this codebase!): secrets interpolated into logs/prompts,
LLM output flowing into
exec/eval/shell, tool dispatch without allowlist, unbounded agent loops without budget.
Starter packs — vendor before writing
coderabbitai/ast-grep-essentials— security rules for 10+ languages, with tests. Plain ast-grep project: clone andast-grep scan --config <pack>/sgconfig.yml ., or copy the relevantrules/<lang>/security/*.yml(MIT) into.ast-grep/rules/.raxITlabs/agent-security-review— 58 rules for AI-agent code (prompt injection → sinks, MCP tool poisoning, denial-of-wallet).
Write a custom rule only when no pack covers it.
5. Adoption rules — why guards die
Established findings (Tricorder/Google CACM 2018, Facebook CACM 2019, Johnson et al. ICSE 2013): developers abandon noisy tools — keep effective false positives under ~10%; findings must arrive at review time, not in batch reports after merge (fix rates collapse); every finding must be actionable (message says why + what instead, next to the code).
In practice: severity: error only for near-zero-FP rules — everything else
starts as warning and is promoted after it survives real diffs without
noise. One noisy guard erodes trust in all of them: fix or delete it the
week it annoys, never "later".
6. Wire into the gate
One source of truth: add ast-grep scan (and ast-grep test if rules exist)
to the project's existing gate script / task runner — CI and wf both call
that. In a wf contract it is just a quick check:
verify:
quick:
- {id: guards, kind: hard, command: "ast-grep scan", timeoutMs: 60000}
Report
Rules created (id → what it guards), triage of rejected rules (where they went
instead), scan status on the current tree, gate wiring. Verification for the
user: ast-grep test && ast-grep scan — exit 0.