Follow-up from Flight's ⚠️ gate on #1805. The gate's detection is sound — this is the remediation text only. But it is worse than "incomplete": the advised sequence lands the developer in a state where git status reports clean while the file on disk is still CRLF, so the original defect (dead suite, broken shebang) survives with no remaining signal.
Current text (scripts/check-shebang-eol.mjs, merged dev)
console.error(` ${unpinned.length > 0 ? '2' : '1'}. Run: git add --renormalize -- <path> (a rule alone does not rewrite committed blobs)`);
That repairs the index. It does not repair the working tree. Result: i/lf w/crlf — the #1793 condition.
Measured, not reasoned
Scratch repos reproducing the exact production condition (CRLF blob committed with no rule, then *.js text eol=lf added in a second commit, pre-existing working tree). Blob state read as raw bytes in Node — never pipe git cat-file through PowerShell, it re-encodes line endings and gives a confidently wrong answer.
| # |
Sequence |
HEAD |
index |
worktree |
git status |
|
| A |
git add --renormalize ← what we ship |
CRLF |
LF |
CRLF |
M |
partial |
| B |
A + git checkout -- <path> |
CRLF |
LF |
CRLF |
M |
silent no-op |
| C |
A + commit + git checkout -- <path> |
LF |
LF |
CRLF |
(clean) |
silently wrong |
| H |
A + commit + git checkout --force -- <path> |
LF |
LF |
CRLF |
(clean) |
--force does not force |
| E |
A + commit + delete file + git checkout |
LF |
LF |
LF |
(clean) |
✅ repaired |
| F |
A + commit + git rm --cached -r . + git reset --hard |
LF |
LF |
LF |
(clean) |
✅ repaired |
| G |
A + delete file + git checkout (no commit) |
CRLF |
LF |
LF |
M |
✅ repaired, uncommitted |
Three findings, in increasing order of nastiness
git checkout -- <path> is a no-op here (row B). It looks like the obvious second step. Git skips the write because the stat cache says the file is current.
--force does not force it either (row H). This is the one nobody would guess, and it is the natural escalation after B fails.
- Once the blob is LF,
git status is structurally incapable of reporting a CRLF worktree (rows C/H). The clean filter normalizes CRLF→LF on the way in, so git compares LF-to-LF and sees no difference. The remediation we ship converts a visible, permanently-dirty file into an invisible broken one. That is a strict downgrade — the churn was annoying but at least it was a signal.
Only deleting the file first works. That is the single non-obvious step, and it is absent from the message.
Proposed replacement
console.error(` ${unpinned.length > 0 ? '2' : '1'}. Repair the committed blob:`);
console.error(' git add --renormalize -- <path>');
console.error(` ${unpinned.length > 0 ? '3' : '2'}. Force the working tree to be rewritten -- the step above fixes`);
console.error(' the index only, and the working tree keeps its CRLF copy:');
console.error(' rm <path> && git checkout -- <path>');
console.error(' Deleting first is REQUIRED. `git checkout` alone is a silent no-op (stat');
console.error(' cache), and `--force` does not override it. Once the blob is LF, `git');
console.error(' status` reports clean whether the working tree is CRLF or not -- the clean');
console.error(' filter normalizes on read -- so skipping this step leaves a broken file');
console.error(' with no remaining signal. Verify with a byte check, not `git status`.');
Verification that is actually valid (PowerShell-safe):
node -e "const b=require('fs').readFileSync(process.argv[1]);console.log([...b].filter((c,i)=>c===10&&b[i-1]===13).length+' CRLF')" <path>
Why this belongs on the record beyond the text fix
This is tooling reproducing the defect it was built to catch — third occurrence in this family (the CRLF-shebang lint authored with CRLF; check-shebang-eol's enumeration blind to its own churn class, #1804; now its remediation manufacturing the invisible variant). It is also the sixth instance of a silent no-op masquerading as success in this area, alongside --renormalize staging nothing for the LF-blob/CRLF-worktree mirror case (*.snap).
The generalization worth keeping: for text eol=lf files, git status is not a valid oracle for working-tree line endings in either direction. Any runbook step, gate message, or repair block in this repo that verifies with git status is verifying nothing. Byte-count or blob-SHA only.
Related: #1793 (same condition, reached a different way), #1804, #1805, #1790.
Follow-up from Flight's⚠️ gate on #1805. The gate's detection is sound — this is the remediation text only. But it is worse than "incomplete": the advised sequence lands the developer in a state where
git statusreports clean while the file on disk is still CRLF, so the original defect (dead suite, broken shebang) survives with no remaining signal.Current text (
scripts/check-shebang-eol.mjs, mergeddev)That repairs the index. It does not repair the working tree. Result:
i/lf w/crlf— the #1793 condition.Measured, not reasoned
Scratch repos reproducing the exact production condition (CRLF blob committed with no rule, then
*.js text eol=lfadded in a second commit, pre-existing working tree). Blob state read as raw bytes in Node — never pipegit cat-filethrough PowerShell, it re-encodes line endings and gives a confidently wrong answer.git statusgit add --renormalize← what we shipMgit checkout -- <path>Mgit checkout -- <path>(clean)git checkout --force -- <path>(clean)--forcedoes not forcegit checkout(clean)git rm --cached -r .+git reset --hard(clean)git checkout(no commit)MThree findings, in increasing order of nastiness
git checkout -- <path>is a no-op here (row B). It looks like the obvious second step. Git skips the write because the stat cache says the file is current.--forcedoes not force it either (row H). This is the one nobody would guess, and it is the natural escalation after B fails.git statusis structurally incapable of reporting a CRLF worktree (rows C/H). The clean filter normalizes CRLF→LF on the way in, so git compares LF-to-LF and sees no difference. The remediation we ship converts a visible, permanently-dirty file into an invisible broken one. That is a strict downgrade — the churn was annoying but at least it was a signal.Only deleting the file first works. That is the single non-obvious step, and it is absent from the message.
Proposed replacement
Verification that is actually valid (PowerShell-safe):
Why this belongs on the record beyond the text fix
This is tooling reproducing the defect it was built to catch — third occurrence in this family (the CRLF-shebang lint authored with CRLF;
check-shebang-eol's enumeration blind to its own churn class, #1804; now its remediation manufacturing the invisible variant). It is also the sixth instance of a silent no-op masquerading as success in this area, alongside--renormalizestaging nothing for the LF-blob/CRLF-worktree mirror case (*.snap).The generalization worth keeping: for
text eol=lffiles,git statusis not a valid oracle for working-tree line endings in either direction. Any runbook step, gate message, or repair block in this repo that verifies withgit statusis verifying nothing. Byte-count or blob-SHA only.Related: #1793 (same condition, reached a different way), #1804, #1805, #1790.