Clean BOM Senior π§Ήβ¨#
Removes invisible UTF-8 BOM and Windows CRLF from source files β and knows
when it must not. Atomic, byte-exact, CI-friendly: a file is rewritten only
when cleaning is genuinely needed and genuinely safe, and never when its BOM
is load-bearing.
A UTF-8 BOM (EF BB BF) at the top of a PHP file breaks header(), sessions
and JSON APIs; CRLF line endings from a Windows editor pollute diffs and
shell scripts. But the naive fix β "strip every BOM" β corrupts files:
for UTF-16/UTF-32 the BOM is the format, and for a non-ASCII .txt/.csv
it is the only thing that makes Excel and Windows Notepad read the file as
UTF-8 instead of mojibake.
Clean BOM Senior v3 answers two questions per file, by its actual bytes,
before writing anything:
Is cleaning needed at all? β byte-exact whole-file detection (a clean
file is never rewritten: inode, mtime and hard links stay intact).Is the BOM perhaps required? β the Smart BOM Policy:
UTF-16/32 and binary files are refused unconditionally, invalid UTF-8 is
protected, "sensitive" text keeps its BOM with an explanation, and code
files (php/js/css/html/xmlβ¦) get cleaned. Every decision is logged with a
reason and available as JSON.
Quick start#
# Anywhere (npm, Node β₯ 18 β Linux, macOS, Windows):
npm install -g clean-bom-senior
bom --dry-run # explain what would change and why
bom # clean the current tree, smart & safe
# POSIX (no npm needed):
curl -fsSLO https://raw.githubusercontent.com/paulmann/Clean_BOM_Senior/main/clean-bom-senior.sh
chmod +x clean-bom-senior.sh
./clean-bom-senior.sh --self-test # verify the tool on THIS machine
./clean-bom-senior.sh # clean the current tree
# CI gate β fail the build when anything needs cleaning:
bom --check # exit 10 = dirty, 0 = clean
What you get#
| π§ Smart BOM Policy | Never strips a BOM the file requires: UTF-16/32 refused unconditionally, NUL-binary refused, invalid UTF-8 protected, Excel/Notepad-sensitive text (txt csv ps1β¦, non-ASCII) kept with an explanation β --force to override the soft cases. Full decision table β |
| π¬ Byte-exact detection | Whole-file scans, no sampling windows: no false positives (v2 rewrote files whose hex dump merely contained 0d0a), no false negatives (v2 missed CRLF past byte 1024). |
| π‘οΈ Atomic & verified writes | Temp file in the same directory β content verification β rename(2). Hard links rewritten in place, symlinks resolved, permissions/ownership/timestamps preserved. A crash can never leave a half-written file. |
| π€ CI-native | --check (exit 10), --json report, --strict, --quiet, NO_COLOR, deterministic output, git-hook ready. |
| π« Sane scope | .git, .svn, .hg, node_modules excluded by default; --ext/--add-ext/--exclude/--exclude-dir/--max-size/--git to shape the rest. |
| π Self-updating | --check-update (exit 11) and --update with content verification; mirrors via CLEAN_BOM_UPDATE_URL; npm installs are redirected to npm. Details β |
| π Comprehensive help | --help plus 12 topics: --help bom-policy, --help ci, --help json, β¦ |
Implementations β one contract#
| Implementation | Version | Platforms | Status |
|---|---|---|---|
clean-bom-senior.sh |
3.0.0 | Linux, macOS, *BSD, WSL, Git Bash (bash β₯ 3.2, GNU and BSD userland) | reference |
bin/bom.js β npm bom / clean-bom-senior |
3.0.0 | everywhere Node β₯ 18 runs, incl. native Windows | native CLI |
clean-bom-senior.ps1 |
3.0.0 | PowerShell 7.6+ β Windows, Linux, macOS. No external tool required | full port |
clean-bom-senior.bat |
2.07.0 | Windows cmd.exe (no PowerShell, no Git Bash) |
legacy, frozen at the v2 contract |
The three v3 implementations are pinned byte-for-byte by two differential
tests β node tests/node/run-tests.mjs differential (sh β node) and
python3 tests/ps/differential.py (sh β ps1, 77 scenarios). On Windows, use
the npm CLI or the PowerShell port; the batch port remains for hosts with
neither, and is a v2.07 tool β see docs/BAT-PORT.md
Β§8 for why it was not carried to v3.
Contract: docs/CLI-CONTRACT.md.
Installation#
npm (recommended β all platforms, incl. Windows)#
npm install -g clean-bom-senior
bom --version # β bom version 3.0.0 (Node.js implementation)
clean-bom-senior --help # same tool, longer alias
POSIX shell β from source#
git clone https://github.com/paulmann/Clean_BOM_Senior.git
cd Clean_BOM_Senior
sudo install -m 755 clean-bom-senior.sh /usr/local/bin/clean-bom-senior
clean-bom-senior --self-test
POSIX shell β one-liner#
curl -fsSL https://raw.githubusercontent.com/paulmann/Clean_BOM_Senior/main/clean-bom-senior.sh \
-o clean-bom-senior.sh && chmod +x clean-bom-senior.sh
PowerShell 7.6+ β from source (Windows, Linux, macOS)#
No external tool is needed for cleaning; everything is .NET byte I/O.
git clone https://github.com/paulmann/Clean_BOM_Senior.git
cd Clean_BOM_Senior
./clean-bom-senior.ps1 --self-test # 10 fixtures, verifies this host
./clean-bom-senior.ps1 --help bom-policy # the Smart BOM Policy in detail
# optional: put it on PATH
Copy-Item clean-bom-senior.ps1 "$HOME/.local/bin/clean-bom-senior.ps1"
The file is pure ASCII on purpose, so
./clean-bom-senior.ps1works
directly on Unix β a UTF-8 BOM in front of the shebang would make the kernel
fall back to/bin/sh. The built-in help still emits the reference's em
dashes and arrows: they are stored as placeholders and restored on output.
Seedocs/PS-PORT.mdΒ§5.
Self-update (all three v3 implementations)#
clean-bom-senior.sh --check-update # exit 11 when a newer release exists
clean-bom-senior.sh --update # verified atomic self-replace
./clean-bom-senior.ps1 --check-update # same contract, same exit codes
./clean-bom-senior.ps1 --update # verifies the #Requires header + version stamp
npm i -g clean-bom-senior@latest # the npm way (enforced automatically:
# --update refuses npm-managed installs)
Uninstall#
npm uninstall -g clean-bom-senior # npm install
sudo rm /usr/local/bin/clean-bom-senior # manual install
Usage#
clean-bom-senior.sh [OPTIONS] [PATH...]
PATH is a file or a directory (scanned recursively); with no PATH the
current directory is scanned. Options may be interspersed; -- ends option
parsing (so file names may start with -).
The everyday commands#
bom # clean the tree with smart safe defaults
bom --dry-run # what would change, and why (no writes)
bom --check # CI gate: exit 10 if anything is dirty
bom --check --json > r.json # machine-readable gate report
bom src index.php # a directory + a file
bom --git # only git-tracked files
bom -v # per-file log with every decision
Smart BOM Policy in one picture#
ββ UTF-16/32 BOM βββββββββββββΊ REFUSE (never touch)
file bytes ββclassifyββΌβ NUL bytes βββββββββββββββββΊ REFUSE (never touch)
ββ invalid UTF-8 βββββββββββββΊ PROTECT (--force: byte-clean)
ββ UTF-8 BOM + sensitive ext β
β + non-ASCII content ββββββΊ KEEP BOM, fix CRLF, explain
ββ UTF-8 BOM + code/ASCII ββββΊ STRIP BOM (+ fix CRLF)
ββ no BOM, no CRLF βββββββββββΊ CLEAN: not rewritten at all
Details, rationale and references: docs/SMART-BOM.md Β·
bom --help bom-policy.
Flag reference (abridged β full: --help options)#
| Group | Flags |
|---|---|
| Operation | -n/--dry-run -c/--check -f/--fix -v/--verbose -q/--quiet --silent -j/--json --color auto|always|never --log-file FILE |
| Selection | --ext LIST --add-ext LIST --exclude GLOB --exclude-dir NAME --no-default-excludes --max-size 10M --git |
| BOM policy | --bom-policy auto|strip|keep --sensitive-ext LIST --force |
| Transform | --no-bom-clear --no-rn-normalize/--no-crlf-normalize --update-mtime --backup --backup-dir DIR |
| Info | -h/--help [TOPIC] -V/--version --self-test --completion --strict |
| Update | --check-update --update |
Exit codes#
| Code | Meaning |
|---|---|
| 0 | success (clean tree, or everything cleaned) |
| 1 | per-file errors, or --strict saw kept/protected files |
| 2 | usage error |
| 3 | environment problem (deps, temp, update network, npm-managed --update) |
| 4 | critical internal error |
| 10 | --check: files need cleaning |
| 11 | --check-update: update available |
CI / CD#
GitHub Actions#
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm i -g clean-bom-senior
- name: BOM/CRLF gate
run: bom --check --quiet --json > bom-report.json || exit $?
# exit 10 fails the job; upload bom-report.json as an artifact
Pre-commit hook#
#!/bin/sh
# .git/hooks/pre-commit (chmod +x)
clean-bom-senior --check --git --quiet || {
echo "BOM/CRLF issues found. Fix with: clean-bom-senior --git" >&2
exit 1
}
Make#
.PHONY: bom-check bom-fix
bom-check:
@bom --check --quiet
bom-fix:
@bom --quiet
More recipes: bom --help ci.
Why BOMs hurt (the PHP story)#
\xEF\xBB\xBF<?php β invisible BOM
header('Content-Type: application/json');
Warning: Cannot modify header information β headers already sent: the BOM
is output, emitted beforeheader()runs.Broken JSON/XML APIs: consumers choke on
\uFEFF{...}.Concatenated builds and
included files leak BOMs mid-stream.Mixed CRLF/LF trees produce whole-file diffs and break
#!/bin/sh
shebangs (/bin/sh^M: bad interpreter).
And why "strip everything" is also wrong:
UTF-16/UTF-32: the BOM is the endianness signature β removing it
corrupts the file (Notepad's "Unicode".txtis exactly this).Excel / legacy Notepad / Win32 ANSI APIs: BOM-less UTF-8
.csv/.txt
with non-ASCII content renders as mojibake (cafΓ©βcafΓΒ©).Windows PowerShell 5.1: parses BOM-less non-ASCII
.ps1as ANSI β
the script changes meaning or fails.
v3 keeps the BOM exactly where it is load-bearing and strips it exactly where
it is dirt β and tells you which decision it made, per file.
Safety guarantees#
Never rewritten unless needed: clean files keep their inode and mtime
(asserted by tests).Atomic + verified: temp file in the same directory β verification
(BOM gone / CRLF gone / nothing added) βrename(2); rollback copies for
in-place rewrites; no leftovers (asserted).Hard refusals no flag can override: UTF-16/32, NUL-binary
(--forceonly unlocks the soft protections).Metadata: permissions always, ownership as root, timestamps of modified
files by default (--update-mtimeopts out).Hard links & symlinks: links stay links (in-place rewrite + warning);
symlink arguments are resolved; the walk never follows symlinks.Full list:
bom --help safetyΒ·docs/SMART-BOM.mdΒ§4.
Documentation#
| Document | Contents |
|---|---|
docs/SMART-BOM.md |
the policy: decision table, rationale, encoding references |
docs/CLI-CONTRACT.md |
normative v3 contract: flags, exit codes, streams, JSON schema, byte semantics |
docs/UPDATE.md |
auto-update design, verification, mirrors, release checklist |
docs/TESTING.md |
test architecture, coverage map, the two differentials, PowerShell invocation traps |
docs/PS-PORT.md |
the PowerShell port: structure, divergences, traps hit, what it found in the reference |
docs/LLM-AGENT-USAGE.md |
wiring the tool into an LLM agent: skill + rules pointer, per-platform entry points, install when missing |
skills/clean-bom-crlf/SKILL.md |
the portable skill itself, for any agent framework that loads skills |
docs/PROMPT-OTHER-HARNESS.md |
copy-paste prompt that makes a foreign agent adopt the rule (RU) |
CHANGELOG.md |
v3.0.0: every fixed v2 defect, measured |
docs/BAT-PORT.md |
(legacy) how the cmd.exe port works, and why there is no v3 batch port |
docs/RAGRAF-REPORT.md |
(legacy) v2-era audit report |
CONTRIBUTING.md Β· SECURITY.md |
ground rules Β· vulnerability reporting |
Development & testing#
bash tests/sh/run-tests.sh -v # reference suite (167 assertions)
node tests/node/run-tests.mjs # Node suite (161 assertions) + shβnode differential
pwsh -File tests/ps/run-tests.ps1 # PowerShell suite (237 assertions) + shβps1 differential
python3 tests/ps/differential.py # sh β ps1 byte parity, 77 scenarios
bom --self-test # built-in acceptance, any host
./clean-bom-senior.ps1 --self-test
shellcheck clean-bom-senior.sh # 0 findings
bash scripts/check-version-consistency.sh
python3 scripts/gen-ps-help.py --check # ps1 help still derived from the reference
CI matrix: shellcheck β bash suites (Ubuntu, macOS) β Node suites
({18,20,22} Γ {Ubuntu, macOS, Windows}) β PowerShell suites (Ubuntu, macOS,
Windows) β both differentials + help-sync gate β npm pack β version guard.
See docs/TESTING.md.
Requirements#
npm CLI: Node β₯ 18. Nothing else β any OS.
PowerShell port: PowerShell 7.6+ (Core). Nothing else is required for
cleaning;gitfor--git,curlfor--update/--check-update
(Invoke-WebRequestis the fallback),stat/fsutilfor hard-link
detection,chownfor ownership transfer as root.Shell reference: bash β₯ 3.2 (macOS system bash works), standard
userland (find sed awk od grep stat tail wc tr mv cp touch chmod mktemp);
optional:iconv(UTF-8 validation),curl/wget(--update),git
(--git).Legacy ports: PowerShell 7.6+ / Windows 10β11
cmd.exe.
License#
PolyForm Noncommercial 1.0.0 Β© Mikhail Deynekin β deynekin.com