pretooluse-bom-guard.sh is fail-open wherever xxd is not installed — including the CI runner #797
Closed
opened 2026-08-14 17:15:08 +02:00 by timothy
·
1 comment
No Branch/Tag Specified
main
renovate/lucene.net
renovate/dotnet-monorepo
renovate/sqlitepclraw.bundle_e_sqlite3-3.x
renovate/meziantou.analyzer-3.x
renovate/cliwrap-3.x
913-scripted-playout-401
release/v26.15.0-notes
issue-806-guard-populations
scratch/767b-poisoned
scratch/767b-control
release/v26.14.0-notes
release/v26.14.0
docs/510-skill-logo-bug-policy
fix/510-watermark-resolution-policy
fix/629-verdict-classifier-falseopens
fix/609-decisions-edit-token-scope
issue-135-clear-to-none
release/v26.12.0-notes
fix/409b-lastscan-api-parity
fix/401-updatechannel-mirror-422
fix/327-playlist-rename-validation
fix/410-scancancel-log-level
fix/409-447-librariesscreen-neverscanned
fix/338-zap-exit-code
fix/367-plex-budget-message
fix/310-debom-legacy-cs
ci/604-lane-rebalance
feat/388-design-mirror
feat/247-test-ownership
feat/247-primary-action
feat/357-player-owned-playback
feat/357-jellyfin-plugin-poc
fix/289-mcp-hardening
issue58-mcp
feat/244-channels-extract
ci/auto-bump-prod-compose
feat/multi-rerun-collections-api
feat/collections-api
feat/quick-wins
feat/185-docs-part2
feat/140-collections-screen
feat/146-channel-edit
feat/147-classic-ui-link
issue22-renovate-dashboard
feat/91-cutover
feat/63-composite-create
feat/65-library-browse
feat/85-epg
feat/86-schedule-editor
feat/109-dashboard-data
feat/99-session-tracking
fix/dockerfile-node-tag
feat/59-spa-foundation
docs/59-ui-redesign-brief
feat/102-json-guide
feat/111-schedule-durations
feat/104-artwork-upload
feat/103-media-sources-api
feat/playouts-read-api
feat/108-health-api
feat/105-picker-list-endpoints
issue-97-channel-state-api
issue42-jellyfin-musicvideos
issue46-rest-api-error-contract
dependabot/nuget/ErsatzTV.FFmpeg.Tests/multi-d307a2e06f
qsv-improvements
hdr-vulkan-cuda-test
v26.16.0
v26.15.0
v26.14.0
v26.13.0
v26.12.0
v26.11.0
v26.10.0
v26.9.0
v26.8.0
v26.7.0
blazor-final
v26.6.0
v26.5.0
v26.4.0
v26.3.1
v26.3.0
v26.2.0
v26.1.1
v26.1.0
v25.9.0
v25.8.0
v25.7.1
v25.7.0
v25.6.0
v25.5.0
v25.4.0
v25.3.1
v25.3.0
v25.2.0
v25.1.0
v0.8.8-beta
v0.8.7-beta
v0.8.6-beta
v0.8.5-beta
v0.8.4-beta
v0.8.3-beta
v0.8.2-beta
v0.8.1-beta
v0.8.0-beta
v0.7.9-beta
v0.7.8-beta
v0.7.7-beta
v0.7.6-beta
v0.7.5-beta
v0.7.4-beta
v0.7.3-beta
v0.7.2-beta
v0.7.1-beta
v0.7.0-beta
v0.6.9-beta
v0.6.8-beta
v0.6.7-beta
v0.6.6-beta
v0.6.5-beta
v0.6.4-beta
v0.6.3-beta
v0.6.2-beta
v0.6.1-beta
v0.6.0-beta
v0.5.8-beta
v0.5.7-beta
v0.5.6-beta
v0.5.5-beta
v0.5.4-beta
v0.5.3-beta
v0.5.2-beta
v0.5.1-beta
v0.5.0-beta
v0.4.5-alpha
v0.4.4-alpha
v0.4.3-alpha
v0.4.2-alpha
v0.4.1-alpha
v0.4.0-alpha
v0.3.8-alpha
v0.3.7-alpha
develop
v0.3.6-alpha
v0.3.5-alpha
v0.3.4-alpha
v0.3.3-alpha
v0.3.2-alpha
v0.3.1-alpha
v0.3.0-alpha
v0.2.5-alpha
v0.2.4-alpha
v0.2.3-alpha
v0.2.2-alpha
v0.2.1-alpha
v0.2.0-alpha
v0.1.5-alpha
v0.1.4-alpha
v0.1.3-alpha
v0.1.2-alpha
v0.1.1-alpha
v0.1.0-alpha
v0.0.62-alpha
v0.0.61-alpha
v0.0.60-alpha
v0.0.59-alpha
v0.0.58-alpha
v0.0.57-alpha
v0.0.56-alpha
v0.0.55-alpha
v0.0.54-alpha
v0.0.53-alpha
v0.0.52-alpha
v0.0.51-alpha
v0.0.50-alpha
v0.0.49-prealpha
v0.0.48-prealpha
v0.0.47-prealpha
v0.0.46-prealpha
v0.0.45-prealpha
v0.0.44-prealpha
v0.0.43-prealpha
v0.0.42-prealpha
v0.0.41-prealpha
v0.0.40-prealpha
v0.0.39-prealpha
v0.0.38-prealpha
v0.0.37-prealpha
v0.0.36-prealpha
v0.0.35-prealpha
v0.0.34-prealpha
v0.0.33-prealpha
v0.0.32-prealpha
v0.0.31-prealpha
v0.0.30-prealpha
v0.0.29-prealpha
v0.0.28-prealpha
v0.0.27-prealpha
v0.0.26-prealpha
v0.0.25-prealpha
v0.0.24-prealpha
v0.0.23-prealpha
v0.0.22-prealpha
v0.0.21-prealpha
v0.0.20-prealpha
v0.0.19-prealpha
v0.0.18-prealpha
v0.0.17-prealpha
v0.0.16-prealpha
v0.0.15-prealpha
v0.0.14-prealpha
v0.0.13-prealpha
v0.0.12-prealpha
v0.0.11-prealpha
v0.0.10-prealpha
v0.0.9-prealpha
v0.0.8-prealpha
v0.0.7-prealpha
v0.0.6-prealpha
v0.0.5-prealpha
v0.0.4-prealpha
v0.0.3-prealpha
v0.0.2-prealpha
v0.0.1-prealpha
Labels
Clear labels
ad-hoc
api
bug
ci-cd
content
dependencies
enhancement
frontend
in-progress
jellyfin
parked
priority: high
priority: low
priority: medium
review
security
One-off / ad-hoc work not tracked by a dedicated issue
REST API / HTTP endpoints
Something isn't working
Build, test, deploy pipeline
Channel content / schedules / playlists
Dependency updates (Renovate)
New feature or improvement
ChicoryTV React SPA frontend
Claimed by an active session — do not pick up
Jellyfin tuner / IPTV integration
Excluded from automatic queue pickup; work only when explicitly selected
Adversarial review finding
Security / vulnerability fix
No labels
priority: high
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: timothy/ersatztv#797
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Found while landing #776, which added an assertion that every hook must reach a real decision. On the Linux CI runner that assertion failed for this hook, and the reason is a live defect rather than a test problem.
The defect
.claude/hooks/pretooluse-bom-guard.shdetects a BOM with:xxdships with vim and is absent on this repo's Linux CI runner — verified on the runner host directly (command -v xxd→ absent;odpresent). Wherexxddoes not exist the substitution yields the empty string, never equalsefbbbf, and the guard allows every BOM in silence. It has been fail-open on any machine without vim since it was written.Impact is bounded in practice — the hook fires in Claude Code sessions, which run on a Mac where
xxdexists — but the guard's correctness has been resting on an undeclared dependency the whole time, and nothing anywhere states it.Why it survived
Two reasons worth recording, because they generalise:
docs/guard-inventory.mdgraded itNONE— no proof it could go red. It was ranked first in that file's "gaps ranked by what a silent failure would let through", on the grounds that the defect it guards has recurred three times (#311, #402, #405). That ranking was right for a worse reason than intended: the guard was not merely unproven, it was already broken.docs/defect-shapes-773.md, with the platform as the thing sampled. Twelve reviewer-passes over #776 shared the same blind spot; only running on Linux exposed it.The fix
od -A n -t x1 -N 3— POSIX, present everywhere, byte-identical output toxxd -pon both platforms (measured).Ships with the proof the guard never had, in
scripts/tests/test_bom_guard_detection.py:xxdremoved from PATH — the regression, and the case every non-vim host was in;Verified RED against the pre-fix hook and green against the fixed one. Inventory row moves
NONE→MUTATION.Done-when
xxddependency removed and the guard proven to detect a BOM without itdocs/guard-inventory.mdrow regraded, with counts updatedClosing record
Outcome: Merged as #798 (squash).
pretooluse-bom-guard.shdetects a BOM withod -A n -t x1 -N 3instead ofxxd -p, and shipsscripts/tests/test_bom_guard_detection.py— the proof it never had. Inventory rowNONE→MUTATION. The twin recipes indocs/decisions/records/process/bom-format-detection-recipe.mdanddocs/contributing.mdare corrected in the same PR.Root cause: the guard depended on
xxd, which ships with vim and is absent on plain Linux hosts including this repo's CI runner. Where it is absent the command substitution yields the empty string, never equalsefbbbf, and every BOM is allowed silently. Fail-open since the guard was written.Decisions/conventions changed:
process.bom-format-detection-recipe— the prescribed detector changes fromxxd -ptood -A n -t x1 -N 3. Its original warning is kept and clarified rather than dropped: the form that once reported all-clean over 19 dirty files wasod -An -c | grep '357 273 277'(octal character output, variable spacing), which is a different invocation of the same tool, not this one.Reusable knowledge:
NONEis not merely untested — it is a guard whose current behaviour nobody has established. This one was ranked first in the inventory's "gaps ranked by what a silent failure would let through". That ranking was right for a worse reason than intended: it was already broken.xxdexists, including twelve reviewer-passes over #776. Unanimity across a sample that shares one blind spot is not evidence. It surfaced only when an assertion demanding the hook reach a real decision ran on Linux.test_DISARMING_..._stops_detectionpassed against the pre-fix hook on Linux — "the mutant is silent" is trivially true when the original is silent too. A mutation proof needs an in-test positive control that the unmutated subject still detects.--no-verify.Verification: 589 tests pass; CI green on every context.
od/xxdequivalence measured over 17 inputs (empty, sub-3-byte, exactly the BOM, BOM+NUL, binary, UTF-16 BOM, unreadable, 20 MB, awkward filenames) on BSDod, GNUodon the runner host, and inside the actual CI runner image — identical in every cell, withxxdconfirmed absent there. New test verified RED against the pre-fix hook and green against the fixed one.Deferred: none. The residual is #785's wider population of unproven guards; this closes one of them.
Docs updated:
docs/guard-inventory.md,docs/decisions/records/process/bom-format-detection-recipe.md,docs/contributing.md.