gitcli.DiffCachedAddedHunks runs git diff --cached -U0 -- <path>. With the pathspec pinned to one side, git renders a rename as a whole-file add — so every pre-existing entry in the renamed file reads as newly added, and the gate sees a well-formed decision that this change did not write.
Measured
Binary built from feat/collapse-decision-layout, hook installed, built binary first on PATH:
git mv docs/decisions-branches/feat__rn.md docs/decisions-branches/feat__renamed.md
+ 401 lines of new Go
guard-commit --layer git-hook → exit 0 ✓ allowed (decision-recorded)
Control, same tree without the rename → exit 65.
Not a regression
git show main:internal/guardcommit/guardcommit.go:177-179 allowed on the staged path alone, so main passes this too. The hunk-based predicate that replaced it did not introduce the hole; it inherited it from the reader beneath. Filing rather than fixing in the PR that found it, because the PR's own change is verified correct and this is a layer down.
Why it is worth fixing anyway
The gate already ships two documented one-line bypasses — [skip-logmind] and LOGMIND_ALLOW_GIT_COMMIT=1. Both are deliberate, scoped to one commit, and visible in the log. This one is none of those: it is silent, it looks like ordinary refactoring, and renaming a decision file is a plausible thing to do while making a substantive change.
It also interacts with the zero-conflict invariant: branch decision files are named after the branch, so a branch rename is exactly when a decision file gets renamed.
Done when
- A staged rename does not present the renamed file's pre-existing content as added.
- The fix is in the reader, not in each caller —
DiffCachedAddedHunks and DiffRangeAddedHunks share this.
- Mutation-tested: the rename case blocks, and a genuine entry added in the same commit as a rename still passes.
- Control the fix against a plain add and a plain modify so the reader has not simply become blind.
Found by the round-17 adversarial panel on #301, which returned MERGE on everything the PR itself changed.
gitcli.DiffCachedAddedHunksrunsgit diff --cached -U0 -- <path>. With the pathspec pinned to one side, git renders a rename as a whole-file add — so every pre-existing entry in the renamed file reads as newly added, and the gate sees a well-formed decision that this change did not write.Measured
Binary built from
feat/collapse-decision-layout, hook installed, built binary first onPATH:Control, same tree without the rename → exit 65.
Not a regression
git show main:internal/guardcommit/guardcommit.go:177-179allowed on the staged path alone, somainpasses this too. The hunk-based predicate that replaced it did not introduce the hole; it inherited it from the reader beneath. Filing rather than fixing in the PR that found it, because the PR's own change is verified correct and this is a layer down.Why it is worth fixing anyway
The gate already ships two documented one-line bypasses —
[skip-logmind]andLOGMIND_ALLOW_GIT_COMMIT=1. Both are deliberate, scoped to one commit, and visible in the log. This one is none of those: it is silent, it looks like ordinary refactoring, and renaming a decision file is a plausible thing to do while making a substantive change.It also interacts with the zero-conflict invariant: branch decision files are named after the branch, so a branch rename is exactly when a decision file gets renamed.
Done when
DiffCachedAddedHunksandDiffRangeAddedHunksshare this.Found by the round-17 adversarial panel on #301, which returned MERGE on everything the PR itself changed.