Repository navigation
Description
The gap
scripts/check-shebang-eol.mjs (added in #1790) enumerates candidate files by #! at byte 0, then checks two things about them: that a .gitattributes rule pins them to LF, and that the committed blob doesn't store CRLF.
But the bug class it exists to prevent is not defined by shebangs. It's defined by eol=lf rule + CRLF blob. Those two sets overlap; neither contains the other.
The consequence is a live false all-clear. Measured on dev @ dbb4c33b:
tracked files : 1811
files with eol=lf attribute: 173
of those, CRLF blob : 2
- docs/src/pages/rss.xml.js (no shebang: gate BLIND)
- packages/squad-cli/src/remote-ui/app.js (no shebang: gate BLIND)
Both are .js, covered by the *.js text eol=lf rule added in #1790 — and neither was renormalized when the rule landed. The gate reports:
Shebang EOL check passed: all 45 shebanged files are pinned to LF.
EXIT=0
Why this half is the dangerous half
A rule over a CRLF blob means git normalizes the working tree to LF while the blob never moves. The diff can never close. The file shows modified in every worktree forever, git restore doesn't stick, and a broad git add sweeps it up — along with whatever real change happens to share the tree. Only git add --renormalize clears it.
Nothing about it is fatal, which is why it survives for years. This is the same condition that made samples/**/*.sh and docs/pagefind.yml perpetually dirty and was misread all session as inert churn.
Scale is irrelevant to the failure: rss.xml.js has one CRLF line. It has presumably sat there for as long as *.js text eol=lf has existed, producing a permanent one-line diff nobody could close by the obvious command.
Fix
Give each invariant its own enumeration, as a union rather than a replacement:
| Invariant | Enumerate over | Why that set |
|---|---|---|
UNPINNED |
files starting with #! |
only the #! walk finds a shebanged file with no rule |
CRLF-BLOB |
files where git check-attr eol = lf |
the churn class is defined by the rule, not by the shebang |
Then renormalize the two files. Both changes are pure EOL — git diff --ignore-cr-at-eol is empty.
Scope boundary (to remain documented, not fixed)
Still not covered: a file that ought to be pinned, has no shebang, and has no rule yet — vitest snapshots were exactly this until *.snap text eol=lf was added. There is no cheap static signature for "some tool writes this file with LF"; guessing would trade a sound check for an unsound one. New tool-written types get pinned by hand, and CRLF-BLOB enforces them from the moment a rule exists.
Measurement note
Count CRs by reading blob bytes directly (git cat-file into a Buffer), or compare blob SHAs. Piping git cat-file through PowerShell re-encodes line endings and yields a confidently wrong number.
Activity
github-actions commentedon Aug 21, 2026
📋 Assigned to Booster (CI/CD Engineer)
Issue: #1804 — check-shebang-eol gate is blind to the churn class it exists to catch (2 CRLF blobs live on dev)
Booster will pick this up in the next Copilot session.
fix(ci): enumerate the EOL blob check by rule, not by shebang (#1805)