The same rule in two files is how agent-written code drifts
A protected-path list was typed by hand and had drifted nine entries behind reality. Derive the list instead of copying it, and gate the copies you keep.
A guard protected a list of directories. The list had been typed by hand, and it was nine entries behind the filesystem.
Not wrong — behind. Every directory on it was still protected. The nine that had been added since were not, and the guard reported nothing, because from its own point of view it was working perfectly.
The case this post is about
One guard, needed in two copy branches of one script, added to one of them:
guards=$(grep -c "$PATTERN" "$FILE")
[ "$guards" -ge 2 ] || fail "guard missing in one of the two copy branches (found $guards/2)"
The script copies assets to a site and has two loops — two source conventions, two code paths. The fix went into the first loop, was tested against the first convention, and shipped.
The second loop kept leaking. The build was green, the tested convention worked correctly, and nothing anywhere asked whether the guard appeared as many times as it needed to.
This post follows that one guard, because a presence check passes on a half-fix and the difference
between grep -q and grep -c is one character.
The symptom
A rule gets stated in one place. Then it needs to be true somewhere else, so it gets stated there too — a validator and a schema, a constant and a migration, a generator and the test that checks the generator.
Both copies are correct on the day they are written. Then one side changes.
The failure is not that a copy is wrong. It is that neither copy can tell it has fallen behind, because each is internally consistent. A list of nine protected directories is a perfectly valid list of nine protected directories. Nothing in it knows there are eighteen.

What made this one expensive is where the drift pointed. A guard that has fallen behind does not fail loudly — it fails open. The directories it had never heard of were simply unprotected, and every audit of the guard passed, because the audits were written against the same list.
Why the obvious fix failed
The obvious fix is to add the missing entries, and I did that first. It is a fix with a shelf life: the next directory added to the project restores the bug exactly, and the person adding it has no reason to know this file exists.
The second attempt is a comment — keep this in sync with the directory layout — which is the weakest instrument in the entire toolbox. It has no failure mode. It cannot be violated, only ignored, and it is ignored by everyone who never opens the file, which is everyone.
The third is code review, and this is where agent-written code changes the calculus. A reviewer reads a diff. The drift is not in the diff — the diff adds a directory, and the file that needed updating is not part of the change. The review can be careful, thorough, and completely blind to it, because nothing in what it is looking at refers to the thing that broke.
The actual mechanism
The fix that holds is to stop having a second copy: derive the list from the thing it describes.
_DERIVED="$(
ls -d "${ROOT}"/workspaces/*/*/idea-layer/*/ 2>/dev/null \
| while IFS= read -r d; do basename "$d"; done \
| sort -u | paste -sd'|' -
)"
_DIRS="${_STATIC}|${_DERIVED}"

A directory becomes protected the moment it exists. Nobody edits this file again.
Two details matter more than the derivation itself.
It unions with the old list, never replaces it. If the glob matches nothing — a fresh clone, a different checkout layout, a CI runner with no workspaces — the derived half is empty and the guard falls back to exactly its previous strength. A derivation that can silently evaluate to nothing is a fail-open dressed as a fix, and this one can only ever widen coverage.
The static list stays as the floor. It is no longer the source of truth, it is the guarantee that the mechanism degrades to safe rather than to off.
The fix for the copies you cannot remove
Derivation is not always available. Sometimes the same rule genuinely has to be written twice, because the two places are different languages, different repositories, or different runtimes.
Then the answer is to gate that the copies agree, and the gate has to count.
I hit this the same day, in a file that copies posts to a site. It had two branches — two source conventions, two loops — and a guard was needed in both. Fixing one branch and not the other produces a build that works, ships correctly for the convention you tested, and silently leaks for the one you did not.
So the check does not ask is the guard present. It asks how many:
guards=$(grep -c "$PATTERN" "$FILE")
[ "$guards" -ge 2 ] || fail "guard missing in one of the two copy branches (found $guards/2)"

A presence check passes on a half-fix. A count does not, and the difference is one character of shell.
The general version of that is worth having, because "how many" is only the right question when you know what the denominator is — and the denominator is itself derivable:
# Count the sites that NEED the guard, then require the guard at all of them.
# A hardcoded expectation rots: add a third branch and a `-ge 2` check passes
# on two-thirds coverage, silently, forever.
check_parity() {
local file="$1" site_pattern="$2" guard_pattern="$3" label="$4"
local sites guards
sites=$(count_matching "$site_pattern" "$file")
guards=$(count_matching "$guard_pattern" "$file")
if [ "$sites" -eq 0 ]; then
echo "WARN: no sites matched '$site_pattern' in $file — is the pattern stale?" >&2
return 1 # zero sites is suspicious, not a pass
fi
if [ "$guards" -lt "$sites" ]; then
echo "FAIL: $label — $guards guard(s) for $sites site(s) in $file" >&2
list_matching "$site_pattern" "$file" >&2
return 2
fi
echo "ok: $label — $guards/$sites"
return 0
}
Run against the copy script, before and after the second branch was fixed:
$ check_parity scripts/sync-assets.ts 'for (const ext of' 'if (!referenced(' 'extension guard'
FAIL: extension guard — 1 guard(s) for 2 site(s) in scripts/sync-assets.ts
18: for (const ext of LEGACY_EXTS) {
41: for (const ext of ASSET_EXTS) {
$ check_parity scripts/sync-assets.ts 'for (const ext of' 'if (!referenced(' 'extension guard'
ok: extension guard — 2/2
The zero-sites branch is the part I would not have written the first time. A parity check whose site
pattern stops matching — because the loop was refactored into a map, say — reports 0 guards for 0 sites, which arithmetic calls equal and which means the check has quietly stopped checking. Treating
an empty denominator as suspicious rather than satisfied is the difference between a gate and a
decoration.
The same shape covers the case where the copies live in different repositories. Here, a fix to a module that came from a shared template is not allowed to be a silent local fix — it has to flow back to the template, or the template and every other project built from it keep the defect. The local fix is permitted; the silent local fix is not.
How we proved it
The derived list, audited against a real project:
typed by hand: 9 subtrees
on disk: 18 subtrees
unprotected: 9 (including i18n strings and the server schema)
Two of the nine were unambiguous sources of truth, which is the part that turns this from tidiness into a defect worth writing about.
And the counting check, against a deliberate half-fix — one branch guarded, one not:
guard missing in one of the two copy branches (found 1/2)
That is a canary I wrote by breaking the fix on purpose, and it is the only reason I know the check distinguishes a whole fix from half of one. The version I would have shipped without it asked whether the guard appeared anywhere in the file, and a half-fix satisfies that.
The honest limitation: deriving from the filesystem trades one coupling for another. The guard now depends on a directory layout, and if that layout is reorganised the glob silently matches nothing — at which point the union-with-static design is the only thing standing between a refactor and an unprotected tree. I have the floor, but I do not have an alarm. A derivation that yields dramatically fewer entries than last time is a signal, and I am not currently checking for it.
Why agents make this sharper
None of this is new. What changed is the rate.
An agent writing code produces more of it, faster, and in more places per unit of intent than a person does. A single instruction can touch a generator, its template, its test and its documentation — four statements of one rule, all correct, all created in the same minute, all free to drift the moment any one of them is edited alone.
It is also, in my experience, better than a person at the local consistency check and worse at the global one. Ask whether a file is internally coherent and the answer is reliable. The drift lives precisely where nothing is internally incoherent.
So the duplication that a human codebase accumulates over years arrives in an afternoon, and the mechanism that catches it has to be mechanical, because the rate has outrun the review.
What this generalizes to
When a rule needs to be true in two places, try to delete one of them — derive it, generate it, or import it, so there is nothing to keep in sync. Where the copy is unavoidable, do not write a comment asking people to remember; write a check that counts the copies and fails when they disagree.
And give any derivation a floor. The purpose is to fail safe when the world changes shape, because the world will change shape, and the derivation will be the last thing anyone thinks to look at.
One post a week, no digest
New writing when it lands. No roundups, no course pitch, no "10 tips".