Repository navigation
Fix npm CRLF-shebang bin hash checks (#1756) - #1757
Conversation
Assisted-by: Claude Code:claude-opus-5-5
On Linux and macOS npm's bin-links rewrites a bin file whose first
line is "#!...\r\n" to end in "\n". socket-patch compared the installed
bytes against the tarball's hashes, so a pristine npm install of such
a bin failed apply as drift, an overwrite with consent wrote back a
shebang env can't run ("node\r": exit 127), hosted vex omitted a
correctly patched install, and vendored vex warned the tree was out
of sync.
The hash checks now also accept npm's form of the original and the
patched content, and apply and rollback write the bytes in that form,
so the bin keeps running. An overwrite with consent of an npm bin with
an LF shebang writes the patched content the same way.
Fixes #1756
Assisted-by: Claude Code:claude-opus-5-5
A superseding patch may replace content an earlier Socket patch wrote. When npm installed that earlier patch with an LF shebang, its hash named no Socket blob, so the update was refused as a local edit. The check now also tries npm's original form of the file. Hashing npm's original form now reads a file whole only when its first 2 KB start with a shebang line, so a mismatched jar or wheel is not read twice. Refs #1756 Assisted-by: Claude Code:claude-opus-5-5
- Write a bin's recorded bytes when they are not valid UTF-8: npm would replace them with U+FFFD, a form nothing can trace back to the hash. - Roll back a bin the patch added when npm installed it with an LF shebang, as apply and vex already count it patched. - Keep the stale-install probe in step with vex for such bins. - A project rollback now sees a global copy still patched in npm's form and points at `rollback -g`. - On Windows npm leaves bins alone, so an overwrite there writes the patch's exact bytes. - One helper checks the blob and builds npm's form for both writers. Refs #1756 Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
When a patch's own bytes already gave a CRLF-shebang bin an LF shebang, apply wrote them as recorded, and rollback then restored the CRLF tarball original: the bin failed with "env: 'node\r'". Rollback and apply now both decide from the file on disk: off Windows, an npm package's bin with an LF shebang gets npm's form of whatever is written; elsewhere only a file that is provably npm's form of the content it replaces does. Refs #1756 Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
|
Labeled Ready for review at
Generated by Claude Code |
|
Ready for review at
Generated by Claude Code |
On Windows npm leaves bin files alone, so form_for_write writes the patch's exact CRLF bytes there and this test's NPM_BIN_AFTER assertion fails. Windows unit tests only run in the merge queue, so the failure would evict the PR there. Gate it like its sibling consent_overwrite_of_npm_bin_keeps_lf_shebang. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Final review: one blocking issue, fixed in 5831b1a.
Checked locally: I removed bugbot run Generated by Claude Code |
|
bugbot run Generated by Claude Code |
1 similar comment
|
bugbot run Generated by Claude Code |
|
Ready for review at
Generated by Claude Code |
|
Ready for review again at
Generated by Claude Code |
Final review briefWhat it does. npm's bin-links Risk: low.
Look here
Verified
Changes I made: none. Open questions (none block merging; could go in a follow-up):
Auto-merge is armed, so approving sends this straight to the merge queue. Generated by Claude Code |
npm's bin-links rewrites a bin's CRLF shebang on Windows as well: shim-bin.js runs fixBin right after writing the cmd-shim. The writers assumed npm left bins alone there, so a consent overwrite or a rollback on Windows wrote the recorded CRLF bytes instead of npm's form. Verify accepted both, so nothing broke, but the files no longer matched what npm installs. Drop the Windows exception and the test gates that assumed it. Refs #1756 Assisted-by: Claude Code:claude-opus-5-5
|
[agent] Final-review open question 1 confirmed: npm's bundled Generated by Claude Code |
|
BugBot review Generated by Claude Code |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 1206981. Configure here.
|
[agent] Final reviewer: disarmed auto-merge. The head moved to Generated by Claude Code |
Final review brief (updated for
|
v5.0.1 could write a patch's raw bytes over an npm bin that npm had installed with an LF shebang, leaving "#!/usr/bin/env node\r" that env can't run (exit 127). After #1757, upgrading did not recover it: apply and apply --check called it already patched, and rollback wrote the CRLF original back, because the shebang form was chosen from whatever was on disk. Decide from the package's own package.json bin field (bin string or object, or directories.bin) whether a file is a bin npm rewrites. Apply now rewrites such a bin in npm's form, apply --check reports it as not applied, and rollback restores npm's runnable form of the original. Fixes #1791 Assisted-by: Claude Code:claude-opus-5-5
LLM Description written by Claude Code:claude-opus-5-5
Fixes #1756
Summary
npm's
bin-links(fixBin, which pnpm also uses) rewrites, on every OS, everybinfile whose first line is#!…\r\nso that line ends in\n. A bin published from Windows is therefore one byte different on disk from the tarball and from the patch'sbeforeHash/afterHash. socket-patch treated that byte as user drift:apply/scan --mode agentfailed withapply_failed("File hash does not match expected value") on a pristine install.--allow-patch-conflicts/--force, and the v5.0.1 default) wrote the CRLF shebang back, so/usr/bin/env: 'node\r'exited 127 while socket-patch reported success.vexomitted a correctly patched install (hash_mismatch, thenno_applicable_patches).vexwarnedvendored_tree_out_of_syncon a tree that was in sync.Root cause
Every hash check compared the raw installed bytes with the tarball hashes. None of them modelled npm's own deterministic install transform.
Fix
patch::npm_binmodelsfixBinexactly: a^#[^\n]+\r\nmatch within the first 2048 bytes, and only the first\ris dropped. It also has the inverse (unfix_bin) and a cheapsource_hash(path), which reads a whole file only when its 2 KB prefix qualifies.verify_file_patch(used by agent apply, hosted vex, vlt, and vendored vex's live-tree in-sync check) now also accepts npm's form ofafterHash(already patched) and ofbeforeHash(ready). This is checked only when the raw hash matches neither.npm_bin::form_for_write, which looks at the file on disk. An npm package's file with an LF shebang gets npm's form of whatever is written, whether patched bytes, a consent overwrite or a restored original, so the bin stays runnable. In other ecosystems this happens only when the file is provably npm's form of the content it replaces. The blob is hash-checked before the transform, asapply_file_patch_atwould check it.verify_file_rollback, including patch-added files, and the store-copy ownership probe) accepts npm's form the same way, so an agent apply→rollback round trip restores npm's form of the original. The CLI's other-scope probe forrollbackalso counts a copy in npm's form as patched.MismatchPolicy::SocketContent, npm's form of an earlier Socket blob still counts as Socket content, so superseding updates work.judge_installed_record(the gem/python stale-install probe) stays in step withverify_patch_record, as its contract test requires.The fallback in verify is not limited to npm purls (
verify_file_patchtakes no ecosystem). It only matches content that differs from the recorded bytes by exactly that one shebang\r, so it can't mask a code change.The wrappers under
npm/,pypi/andgem/only dispatch to the binary and don't need changes.Test evidence
Red on
main(the fix files reverted, the new tests kept):Green on this branch:
cargo test -p socket-patch-core --all-features --lib -- npm_fixed npm_bin crlf_shebang unfixed_crlf consent_overwrite socket_content judge_installed rollback_of_lf rollback_outside: 21 passedcargo test -p socket-patch-cli --all-features --test e2e_redirect_npm_build -- --ignored npm_agent_patches_crlf: passed with real npm 10.9.4. It coversnpm installof a local CRLF-bin tarball, agentapply --offline, the bin printinghi, a no-op re-apply,vexattestingnot_affected, androllbackrestoring npm's form with the bin still runnable.cargo fmt --all -- --check,cargo clippy --workspace --all-features -- -D warnings: cleancargo test --workspace --all-features --no-fail-fast: 228 of 240 test binaries passed. The 12 failing tests are environmental and outside this diff. Nine are write-failure tests (partial_lockfile_write_failure_exits_1_and_writes_no_ledger,relax_loop_must_not_traverse_symlinked_root,an_unremovable_hidden_lock_keeps_every_store_entry,redirect_json_mode_write_failures_emit_error_envelope, tworepair_*,wire_write_failure_maps_error_and_leaves_lock_untouched,wire_failure_rolls_back_already_written_files,scan_redirect_vlt_heal_invalidation_failure_warns) that can't induce EACCES because this sandbox runs as root. Three aree2e_bun_lockbtests: one needs the livepatches-api.socket.dev, which the sandbox proxy refuses, and two depend on the CI-pinned bun (local bun is 1.4.4). CI is the authority for these.node --test npm/socket-patch/bin/socket-patch.test.mjs: 24 passed (wrappers untouched)Per-issue checklist (#1756)
verify_accepts_npm_fixed_crlf_shebang,apply_patches_npm_fixed_bin_and_keeps_it_runnable, e2econsent_overwrite_of_npm_bin_keeps_lf_shebang(andconsent_overwrite_outside_npm_writes_exact_bytesas the guard)applied_when_npm_fixed_the_patched_bin_shebang(the sameverify_patch_record_forpath the hosted verifier uses)vendored_tree_out_of_sync: the live-tree in-sync probe is the sameverify_patch_record_for/verify_file_patchcall, covered by the test aboverollback_restores_npm_fixed_bin,rollback_deletes_npm_fixed_added_bin, e2e/code-review high and Bugbot
Every
/code-review highfinding is fixed: lossy UTF-8 round trip, the new-file rollback branch, the other-scope rollback probe, thejudge_installed_recordcontract, Windows gating, the duplicated blob check, and the repeated hashing. Bugbot's first review found that rolling back a patch whose own bytes gave the bin an LF shebang restored the CRLF original. It is fixed in aea403e, which also removes the "aReadywhosecurrent_hashis not the expected hash" inference that/code-reviewhad flagged. Both writers now decide from the file on disk. Regression tests:rollback_of_lf_patched_bin_restores_npm_formandrollback_outside_npm_restores_exact_bytes.Follow-ups
fixBinthere too, right after writing the cmd-shim (bin-links5.0.0lib/shim-bin.js:78), as the final review pointed out. 1206981 drops the Windows exception and the test gates that assumed it. Before that, a consent overwrite or a rollback on Windows wrote the recorded CRLF bytes; verify accepted them, so nothing broke. No real-npm Windows e2e covers this; the unit tests now run there.MissingBlob); (3) the write side treats any LF-shebang file of an npm package as a bin, without readingpackage.jsonbin.https://claude.ai/code/session_011f9aeS6yNpFr3Yme2w9kfw
Note
Medium Risk
Changes core hash verification and file writes for npm bins across apply, rollback, and VEX; logic is narrow (one-byte shebang transform) but touches security-sensitive patch paths.
Overview
Fixes #1756: npm
bin-linksdrops the\rfrom a Windows-style#!…\r\nshebang on install, so pristine bins no longer match tarballbeforeHash/afterHashand patch apply, VEX, and rollback misbehaved.Adds
patch::npm_binto mirrorfixBin(and invert it for hashing). Verify paths now treat npm’s on-disk LF-shebang form as matching recorded before/after hashes viasource_hash. Apply and rollback write throughform_for_writeso npm bins stay LF-shebang and remain runnable instead of writing CRLF back. VEX / stale-install checks and SocketContent supersede logic follow the same rules; the CLI rollback “other scope still patched” probe also counts npm-form patched bins (Ready).Coverage is mostly new unit tests plus an ignored Unix e2e that runs a real
npm installthrough apply, vex, and rollback.Reviewed by Cursor Bugbot for commit 1206981. Configure here.
Generated by Claude Code