chore: widen the BOM-guard remedy to staged + dirty files (review nit)
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 5s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 6s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m18s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 14s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 14s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 3m9s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 13m30s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped

Cold review (MERGEABLE-WITH-NITS) flagged that the printed fix snippet and the
docs by-hand check used `git diff origin/main...HEAD`, which excludes
staged-but-uncommitted files. A brand-new staged .cs is exactly what fires the
pre-commit deny yet is absent from that diff, so the copy-paste remedy would strip
nothing → deny again → loop. (The deny still names the file, so manual stripping
always worked; this just makes the printed remedy match the guard's own detection
set: branch diff ∪ staged ∪ dirty.)

Verified on a fresh fixture: a brand-new staged BOM file is denied, and the
widened snippet strips it.

Left as accepted (fail-open, Medium, CI backstops; shared with sibling hooks):
`git -C <path>` isn't parsed for the target tree (only `cd` is — matches the
`cd <wt> && git` usage here), and the heredoc-body false-deny vector that
bash-guard/worktree-guard also carry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-17 17:03:02 +02:00
co-authored by Claude Opus 4.8
parent 9e1fce1a3a
commit 030a7c19e9
2 changed files with 9 additions and 5 deletions
+8 -4
View File
@@ -81,10 +81,14 @@ Strip it, then re-run this command:
python3 - <<'EOF' python3 - <<'EOF'
import subprocess import subprocess
fs = subprocess.run(['git','diff','--name-only','origin/main...HEAD','--','*.cs'], def g(*a): return subprocess.run(['git','diff','--name-only',*a,'--','*.cs'],
capture_output=True, text=True).stdout.split() capture_output=True, text=True).stdout.split()
for f in fs: # same detection set as the guard: branch diff + staged + dirty (a brand-new staged
b = open(f,'rb').read() # file is exactly what fires the deny and is absent from origin/main...HEAD)
fs = set(g('origin/main...HEAD')) | set(g('--cached')) | set(g())
for f in sorted(fs):
try: b = open(f,'rb').read()
except OSError: continue
if b[:3] == b'\xef\xbb\xbf': if b[:3] == b'\xef\xbb\xbf':
open(f,'wb').write(b[3:]); print('stripped', f) open(f,'wb').write(b[3:]); print('stripped', f)
EOF EOF
+1 -1
View File
@@ -117,7 +117,7 @@ talks exclusively to the REST API. Every former Blazor route now 302-redirects t
`io.open(..., encoding='utf-8-sig')` *writes one back*, and sed/perl round-trips keep it. This cost `io.open(..., encoding='utf-8-sig')` *writes one back*, and sed/perl round-trips keep it. This cost
two sessions a red CI job on 2026-07-17 alone (PR #405, 6 files; PR #402, 19), which is what the two sessions a red CI job on 2026-07-17 alone (PR #405, 6 files; PR #402, 19), which is what the
guard is for. To check by hand: guard is for. To check by hand:
`for f in $(git diff --name-only origin/main...HEAD -- '*.cs'); do head -c3 "$f" | xxd -p | grep -q '^efbbbf' && echo "BOM: $f"; done` `for f in $(git diff --name-only origin/main...HEAD --cached -- '*.cs'; git diff --name-only -- '*.cs') ; do head -c3 "$f" | xxd -p | grep -q '^efbbbf' && echo "BOM: $f"; done` (covers branch + staged + dirty — a brand-new staged file is absent from `origin/main...HEAD`)
(note `dotnet format --include` needs **bash** — `mapfile` is bash-only, and under zsh the file list (note `dotnet format --include` needs **bash** — `mapfile` is bash-only, and under zsh the file list
comes out empty, which looks exactly like the tool silently doing nothing). (A one-time repo-wide normalization comes out empty, which looks exactly like the tool silently doing nothing). (A one-time repo-wide normalization
is a separate, unmade decision; the touched-file rule is the standing one.) is a separate, unmade decision; the touched-file rule is the standing one.)