Three Red Commits That Must Not Be Rewritten
A new checker gate flagged three old index commits for a missing Task trailer. Rewriting history was the wrong answer; grandfathering by record was the fix.
TL;DR
Three regen commits to COMMITS.md failed the new Task: trailer gate because they predate the rule. Rewriting history would cascade new hashes and break tooling, and hooks only run at commit time, so retroactive enforcement is impossible. The fix: a GRANDFATHER allowlist keyed by hash with a documented reason, plus a DECISIONS.md rule for all future index commits.
The first checker run after the Task: trailer rule went live ended in three red lines. Commits 6f8c549, d45dbf3, 771704f, all flagged FAIL for a missing trailer.
All three touch exactly one file: COMMITS.md, the machine-generated commit index of KotaPortal. They are regen commits produced by the index tool, written before the trailer rule existed. Their messages are free-form summaries with no Task: line in sight.
They were already pushed when the gate found them.
The wrong first move
The instinct was to fix history. Rewrite the three messages, add the trailer, amend, force-push. Clean output, gate goes green, done.
That instinct does not survive contact with how Git computes identity. A commit hash covers the message, the author, the committer, the tree, and the parents. Change any of it and the hash changes [3]. Change the message of 6f8c549 and it stops being 6f8c549. Every descendant commit has that hash as a parent, so every descendant gets a new hash too, chaining forward to the newest commit on the branch. The git-filter-repo FAQ states it plainly: this chaining is fundamental to Git and nothing can change it [3].
The damage is not theoretical. GitHub's own documentation on removing sensitive data warns that any tooling or automation depending on commit hashes not changing will break or misbehave [4]. Issue references, CI caches, review links, anything holding a pinned hash. Three cosmetic trailer fixes are not worth that.
Why the hook cannot judge the past
The other instinct was to re-run enforcement on the old commits, as if the gate were a linter that could be pointed at history.
Git does not work that way. Hooks fire at commit time. A commit-msg hook receives the proposed log message file and can refuse the commit after inspecting it [1]. A pre-commit hook exiting nonzero aborts the commit before it is created [1]. Both describe the moment a commit is born. Neither runs retroactively on commits that already exist.
And the enforcement has holes by design: --no-verify bypasses both hooks entirely [1]. A gate that only hooks can back is a gate that one flag can silence. Past commits are beyond its reach by construction, not by accident.
Recording the exception
So the fix became an allowlist by record. The checker script gained a GRANDFATHER dict keyed by the three hashes, each mapped to the same stated reason: a COMMITS.md-only regen commit that predates the trailer rule. When the gate finds a hash in that dict, the result is not FAIL but "pass, reason on file". The three commits stay in history untouched. The gate stops complaining about them, and only them. Everything from the rule onward is held to the stricter standard.
The forward half lives in a DECISIONS.md row: every future index-trailing commit must carry its work card's Task: trailer. That is what trailers are for. Git defines them as metadata appended at the end of the otherwise free-form commit message, the simplest form being a key-value pair with a colon separator, and the trailer block must follow a blank line [2]. The message can still tell a story; the structured part stays machine-readable.
What makes an entry honest
A permanent allowlist is only as good as its hygiene. The rule applied to the GRANDFATHER dict: one entry per commit, one stated reason per entry, no expiry date pretending to be a fix, and the whole decision written into the decisions log where the next person will find it. Reasons here refer to the documented workflow pitfall around a final index commit, stated qualitatively rather than quoted from internal docs.
The judgment worth keeping: an exception entry is honest when its reason survives standing alone. If you cannot defend an entry after reading its comment without asking anyone, it is not a record, it is a cover.