Skip to content

check-shebang-eol gate is blind to the churn class it exists to catch (2 CRLF blobs live on dev) #1804

Closed

Description

@bradygaster

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.

Follow-up to #1788 / #1790.

Activity

  1. added
    squadSquad triage inbox — Lead will assign to a member
    on Aug 21, 2026
  2. github-actions commented on Aug 21, 2026

    @github-actions
    Contributor

    📋 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.

    For Copilot coding agent: If enabled, this issue will be worked automatically.
    Otherwise, start a Copilot session and say:
    Booster, work on issue #1804

  3. added a commit that references this issue on Aug 21, 2026
    17c0a3e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    squadSquad triage inbox — Lead will assign to a member

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions