Found by an audit that asked, for every agent-memory claim, "would a rule have caught this?" This entry named its own enforcement gap — which makes it a rule to write, not a lesson to store.
The gap
Nothing in bun run validate notices a throwaway script left at the repository root. A stray fix-sec-test.py reached #500 exactly this way: a helper script was created at the root, the cleanup was chained with && so it never ran when the script exited non-zero, and a later git add -A committed it.
Root-level scratch files are invisible to every existing gate:
- not linted (oxlint covers
src/, tests/, lint/, scripts/, shims/)
- not type-checked (outside
tsconfig.json include)
- not seen by knip (outside
project)
- not matched by any ADR's
files globs
Proposed rule
A companion rule that fails when an unexpected file appears at the repository root, against an allowlist of the roots we actually ship (package.json, README.md, CLAUDE.md, bun.lock, config dotfiles, …). Scripting languages are the common case (.py, .js, .sh, .ts), but the check is cheapest and most durable as "root contents must match the allowlist".
GEN-003-tool-invocation-via-scripts.rules.ts is the closest existing idiom — an ADR-owned rule asserting a property of repository layout rather than of a single source file.
Adjacent habit worth capturing in the same change
The mechanism behind the incident is worth a line in the ADR's Do's, since the rule catches the symptom rather than the cause:
- Never chain temp-file cleanup with
&& — python fix.py && rm -f fix.py skips cleanup precisely when the script fails. Use ; or a trap.
- Prefer
git add <explicit paths> over git add -A when a scratch file may exist.
Found by an audit that asked, for every agent-memory claim, "would a rule have caught this?" This entry named its own enforcement gap — which makes it a rule to write, not a lesson to store.
The gap
Nothing in
bun run validatenotices a throwaway script left at the repository root. A strayfix-sec-test.pyreached #500 exactly this way: a helper script was created at the root, the cleanup was chained with&&so it never ran when the script exited non-zero, and a latergit add -Acommitted it.Root-level scratch files are invisible to every existing gate:
src/,tests/,lint/,scripts/,shims/)tsconfig.jsoninclude)project)filesglobsProposed rule
A companion rule that fails when an unexpected file appears at the repository root, against an allowlist of the roots we actually ship (
package.json,README.md,CLAUDE.md,bun.lock, config dotfiles, …). Scripting languages are the common case (.py,.js,.sh,.ts), but the check is cheapest and most durable as "root contents must match the allowlist".GEN-003-tool-invocation-via-scripts.rules.tsis the closest existing idiom — an ADR-owned rule asserting a property of repository layout rather than of a single source file.Adjacent habit worth capturing in the same change
The mechanism behind the incident is worth a line in the ADR's Do's, since the rule catches the symptom rather than the cause:
&&—python fix.py && rm -f fix.pyskips cleanup precisely when the script fails. Use;or atrap.git add <explicit paths>overgit add -Awhen a scratch file may exist.