Practical guide for a developer on a team where the same mistake keeps coming back. Companion guides: 02 · 03 · 04 · 05 · 06 · 07. Runnable companion: examples/recurring-rule.
Source: Theo's video. Quotes below are from that video unless marked otherwise. Boris quotes are Boris as quoted by Theo in the video.
The idea and where it comes from
Boris's post, as Theo reads it at t=482s: an agent could fix the same issue every time it appears, but that costs tokens and misses cases; if the agent instead writes a lint rule, CI step, or routine, the whole class of issue is automated permanently. Theo agrees at t=496s and adds the economics at t=510s: a custom lint rule that needs 400 lines to check something odd was never worth writing by hand, because code review caught it more cheaply. Now that the code and its tests are cheap to produce, rules like that pay for themselves. He repeats the example of his file-upload service at t=545s.
Theo also names the accounting you should use at t=749s: do not let your agents write your CLAUDE.md or AGENTS.md. This guide is the general form of that: you decide what the rule says; the agent drafts the mechanics. That is a team ownership practice; a prompt alone does not enforce it.
Suggested (not demonstrated in the video): the decision procedure, the cost test, and the acceptance steps below.
When to apply
Write a rule when the expected savings justify the rule's cost. A repeated failure is the normal trigger, but the repeat count is a signal, not a prerequisite: one painful, expensive occurrence justifies a rule on its own, and an explicit request for a standing guard does not need to wait for a second occurrence at all.
- Choose a static rule for a statically detectable failure, or a focused behavioral test for a runtime outcome. Use guide 02 for a complete user journey.
- The cost of the rule is less than the cost of the next few manual fixes, including the agent's correction cost when it makes the mistake again (tokens, review time, waiting).
For a mistake seen once and cheap to fix, note it and wait for a repeat before spending rule-building effort — unless the user has explicitly asked for a guard, in which case record the request and build the smallest one that satisfies it.
Implementation
- Collect the instances. Keep the observed examples: the file, import path or call shape. One consequential failure or an explicit guard request is enough to start; preserve a permitted alternative alongside it.
- Pick the mechanism that fits the constraint, preferring what already exists. Start
from the constraint's shape, not from a fixed cost ladder: an existing lint rule
(for example ESLint's built-in
no-restricted-imports), a type or schema constraint, an existing test, lint or CI configuration, or a conditional method can each carry it; move up to a custom lint rule (writing guide, tutorial) only when no existing mechanism can express the constraint. Extend an existing rule before adding a second mechanism that can drift from the first. - Prove the failure first (red), for executable enforcement. Run the check with the rule absent or disabled and confirm the bad code passes. Keep that output. Without it you cannot later show the rule changed anything. This before/after proof applies to executable checks; an instruction-only change is verified by wording review against the observed failure and, where loading matters, by confirming the instruction is actually loaded.
- Enable the rule and fix the live violations (green). Inspect the findings in the intended scope and repair those violations. For wider adoption, choose an explicit incremental rollout that preserves useful checks and keeps each changed scope usable.
- Ship the rule with adequate fixture coverage: at least one file that must fail and one that must pass. Reuse existing fixtures where they already cover the rule; add a pair only when none does. If the project runs checks in CI, cover the rule there so a future linter upgrade that changes behavior breaks visibly instead of silently; an instruction-only change needs no new fixtures or CI wiring.
Concrete example
A UI component importing the database client directly keeps reappearing. This repository
contains a working demonstration in
examples/recurring-rule: ESLint's built-in
no-restricted-imports configured for src/ui/**, an offending file
(src/ui/UserCard.js), an approved file (src/ui/UserList.js, importing the API layer),
a red configuration with the rule removed, and run-demo.sh which captures the red pass and
the green failure into evidence/. It uses no custom rule and needs no network at run time.
Acceptance
- Red run (rule absent): offending file passes, output preserved.
- Green run (rule enabled): exactly the offending import is reported, by rule name, with a message that says what to do instead; approved imports produce no findings.
- The project's actual check command runs the rule; wire it into existing CI where applicable. Reused or new fixtures cover the prohibited and permitted cases.
Failure modes and maintenance
- Rules nobody remembers. The error message is the documentation. It must name the approved path, as the demo's message does.
- Silent rule rot. Linter major versions change behavior. The fixture pair turns that into a visible CI failure.
- Rule sprawl. Review a rule that fires almost never or covers almost nothing on its merits: retire it when it is redundant, obsolete, or provably wrong, and keep it when it still prevents a real, costly failure. A rare but expensive catch justifies a rule that almost never fires; "delete what rarely fires" is not a maintenance rule on its own.
- Boundary honesty. Static linting is a guardrail, not a security boundary. It cannot prove every bypass impossible (dynamic imports, build-time codegen). Say so in the rule message or README rather than overselling it.
- Escalation order. If the same class of failure keeps slipping past lint, it may be a runtime property — move it to a test, not to a bigger regex.

