Repository navigation
After the #1756 fix, a CRLF-shebang npm bin that v5.0.1 already broke stays broken: apply reports "already patched", and repair and rollback keep the unrunnable CRLF shebang #1791
Description
[agent] Found by the scheduled npm bug-hunt routine (ledger #302).
Summary
#1757 (fixing #1756) makes a fresh agent apply write npm's fixBin form (an LF shebang) for a bin file published with a #!…\r\n first line, and rollback restore that form too. But npm_bin::form_for_write picks the form from the file currently on disk: it writes the fixBin form only when the file already has an LF shebang. A project that v5.0.1 patched (its default overwrite path, or --allow-patch-conflicts on main before #1757) has the raw CRLF afterHash bytes on disk. That is the state #1756 called "the bin exits 127". Upgrading to main doesn't recover it:
apply/scan --mode agent: the raw bytes hash toafterHash, so it reports1 already patched, exit 0, and leaves the bin broken.apply --check: "Patches are in sync", exit 0.repair: "Repair complete.", and the bin is still broken.rollback: the on-disk shebang is CRLF, so it writes the recorded CRLFbeforeHashbytes. The bin still exits 127, and the file now differs from whatnpm installgave (which ran).vex: attests the patch.
I flagged this as a risk on #1756 before #1757 merged (comment). This confirms it on the merged code.
Impact
Every v5.0.1 agent-mode user whose patch touched a CRLF-shebang bin has a CLI that dies with /usr/bin/env: 'node\r': No such file or directory. Upgrading socket-patch, then running apply, repair or rollback, which is the natural way out, changes nothing and reports success. Only a reinstall (rm -rf node_modules && npm ci, then apply) recovers. npm never leaves a CRLF shebang in a bin file (fixBin runs on Windows too, per the npm_bin.rs module doc), so a CRLF shebang on disk in an npm package's bin is never the right form.
Repro
v5.0.1 from npm (npm i @socketsecurity/socket-patch@5.0.1) as $OLD, and a main build as $SP:
#!/usr/bin/env bash
set -u
W=$(mktemp -d); cd "$W"
mkdir pkg && printf '{"name":"crlfbin","version":"1.0.0","bin":{"crlfbin":"cli.js"}}' > pkg/package.json
printf '#!/usr/bin/env node\r\nconsole.log("hi");\r\n' > pkg/cli.js
(cd pkg && npm pack --silent >/dev/null) && mv pkg/crlfbin-1.0.0.tgz .
mkdir proj && cd proj
echo '{"name":"p","version":"1.0.0","dependencies":{"crlfbin":"file:../crlfbin-1.0.0.tgz"}}' > package.json
npm install --no-audit --no-fund >/dev/null 2>&1
gsha() { python3 -c 'import hashlib,sys;b=open(sys.argv[1],"rb").read();print(hashlib.sha256(b"blob %d\0"%len(b)+b).hexdigest())' "$1"; }
cp ../pkg/cli.js before.js; cp before.js after.js; printf '// SOCKET-PATCHED\r\n' >> after.js
B=$(gsha before.js); A=$(gsha after.js)
mkdir -p .socket/blobs && cp before.js .socket/blobs/$B && cp after.js .socket/blobs/$A && rm before.js after.js
cat > .socket/manifest.json <<JSON
{"patches":{"pkg:npm/crlfbin@1.0.0":{"uuid":"e2f99e5d-3afd-5cbd-be80-6a0102bcd6c2","exportedAt":"2026-01-01T00:00:00Z",
"files":{"package/cli.js":{"beforeHash":"$B","afterHash":"$A"}},
"vulnerabilities":{"GHSA-xxxx-yyyy-zzzz":{"cves":["CVE-2099-0001"],"summary":"t","severity":"high","description":"d"}},
"description":"d","license":"MIT","tier":"free"}}}
JSON
$OLD apply --offline; ./node_modules/.bin/crlfbin; echo "bin exit $?" # 127 (the #1756 breakage)
# upgrade to main:
$SP apply --offline; ./node_modules/.bin/crlfbin; echo "bin exit $?"
$SP apply --check --offline; echo "check exit $?"
$SP repair --offline; ./node_modules/.bin/crlfbin; echo "bin exit $?"
$SP rollback --offline; ./node_modules/.bin/crlfbin; echo "bin exit $?"Output on main b4b6d98, npm 10.9.4 / Node 22.22.0 (reproduced three times):
Summary: 1 of 1 targeted patch applied, 0 already patched, 0 not found on disk # v5.0.1
/usr/bin/env: 'node\r': No such file or directory
bin exit 127
Summary: 0 of 1 targeted patch applied, 1 already patched, 0 not found on disk # main apply
/usr/bin/env: 'node\r': No such file or directory
bin exit 127
Patches are in sync (1 patch checked).
check exit 0
Repair complete.
/usr/bin/env: 'node\r': No such file or directory
bin exit 127
Freed 101 B of unused blobs and archives # main rollback
/usr/bin/env: 'node\r': No such file or directory
bin exit 127
Control: the same script without the $OLD step on main gives hi / exit 0 after apply, vex attests, and rollback restores exactly the bytes npm install wrote. So #1757 itself works for fresh trees.
Expected vs actual
- Expected: CLI_CONTRACT.md promises
rollbackreturns the package to its original state, andapply/repairconverge to the patched state. For an npmbinfile, after Fix npm CRLF-shebang bin hash checks (#1756) #1757 that means npm'sfixBinform, a runnable shebang. A file holding the raw CRLFafterHashbytes in a bin is the known-broken output of v5.0.1. Re-applying (orrepair) should rewrite it in thefixBinform, androllbackshould restore thefixBinform ofbeforeHash. - Actual: the CRLF form is treated as correct ("already patched", "in sync"), and
rollbackwrites the CRLF original.
OS × version
| OS | npm | v5.0.1 → main apply |
repair |
rollback |
|---|---|---|---|---|
| Linux | 10.9.4 | fail (already patched, bin 127) | fail | fail (CRLF original, bin 127) |
| macOS | any | untested (same fixBin path; expected fail) |
||
| Windows | any | untested (cmd-shim runs via node, so a CRLF shebang is probably harmless there) |
The first bad release is v5.0.1, which creates the state. Main from ec9aabc (#1757) on doesn't create it, but can't recover from it.
Suspect code
crates/socket-patch-core/src/patch/npm_bin.rs:72-95(form_for_write):npm_installedrequireslf_shebangon disk. For a file that is listed in the package'sbin(or, more simply, any npm-package file whose recorded contentfix_binwould change), the form shouldn't depend on the current on-disk shebang.crates/socket-patch-core/src/patch/apply.rs:275-286:current_hash == after_hashreturnsAlreadyPatchedbefore considering that the npm-installed form would befix_bin(after). A CRLFafterHashmatch on an npm bin could be reported as needing a rewrite (orrepaircould normalize it).crates/socket-patch-core/src/patch/rollback.rs:773: sameform_for_writecall, so rollback writes CRLF.
Activity
mikolalysenko commentedon Oct 28, 2026
[agent] Triage:
priority:p1(npm). Not a duplicate: #1756/#1757 fixed fresh trees; this is the leftover v5.0.1 state, whichnpm_bin::form_for_write(crates/socket-patch-core/src/patch/npm_bin.rs) can't recover because it chooses the fixBin form only when the on-disk shebang is already LF, andapplyreturns AlreadyPatched on the raw CRLFafterHashmatch first.[agent] Claiming this issue (shared root cause: npm bin form chosen from the on-disk shebang instead of the package's bin status, so a CRLF shebang left by v5.0.1 is treated as correct). Branch: agent/fix-npm-bin-crlf-recovery. Claim-ID: 2026-10-28T19:21:35Z-abd076
Generated by Claude Code
mikolalysenko commentedon Oct 28, 2026
[agent] Draft PR: #1792
Generated by Claude Code
Recover npm bins v5.0.1 left with CRLF shebangs
Match npm's bin rules and fix old CRLF rollbacks
npm bughunt run 53: verify #1791 fixed, file #1804
Keep CRLF shebang on bins npm didn't link