RUL-024 — "pre-existing" is a diagnosis, not a disposition

Ruling

"new rule, create a hook, when you output the word prexisting failure print a message that tells you to fix it" (verbatim, 2026-07-28).

Proving that a failing test, warning, leak or defect predates the current change is useful evidence about cause. It is not a decision about what happens next, and it is not permission to leave the thing red.

On labelling any defect "pre-existing" / "already failing" / "unrelated" / "fails on baseline" / "not introduced by this change", exactly one of these must follow in the same turn:

  1. Fix it — root-cause, fix, prove green, say so; or
  2. Surface it as a named blocker — if the fix carries a real behaviour or scope decision that is the user's to make, state the decision explicitly with a recommendation. Not buried in a status line; or
  3. Show it is genuinely unreachable — a third-party binary that cannot be rebuilt, hardware, an upstream repo not under our control — stating why it is unreachable and what was done to establish that.

Restating the label and moving on is the violation. This is the specific shape that project memory feedback-fix-everything-own-everything forbids: authorship and provenance are irrelevant to whether a broken window gets fixed. It sharpens RUL-006 — fix-on-sight already covers "I saw an error"; this covers the narrower dodge of classifying the error so it looks handled.

Evidence

2026-07-28, during the codebase-memory-mcp fork work, three failures were reported across many turns as "pre-existing", each time with correct evidence and each time with no action:

The pattern: the label ended the investigation instead of starting the fix, and in the second case the label was not even established — it was assumed.

Enforcement

Verification

.claude/hooks/no-preexisting-excuse.sh is a Stop hook registered in .claude/settings.json. It reads the last assistant turn from the transcript (thinking and output text) and emits {"decision":"block","reason":...} when it matches any of:

The block returns the three permitted dispositions above, plus up to three context snippets showing what triggered it, so the reminder is specific rather than generic. stop_hook_active short-circuits the hook, so it fires at most once per turn and cannot loop.

Tested on three paths at creation: positive (blocks with reason), negative (clean turn → silent), loop-guard (stop_hook_active: true → silent).