Skip to content

Fix npm bin shebang on unlinked bins (#1804) - #1805

Merged
Mikola Lysenko (mikolalysenko) merged 5 commits into
mainfrom
agent/fix-npm-unlinked-bin-shebang
Oct 29, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 5 commits into
mainfrom
agent/fix-npm-unlinked-bin-shebang

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

LLM Description written by Claude Code:claude-opus-5-5

Fixes #1804

Summary

Since #1792, apply and rollback treated every file a package lists as a bin as one npm had rewritten to an LF shebang at install. npm only does that (fixBin) for bins it links. With bin-links=false (--no-bin-links), or for a directories.bin file of a package installed from a tarball/folder (npm expands that field only in registry packuments), the installed bin keeps its CRLF shebang. So apply wrote bytes matching neither the patch's afterHash nor a fresh install, and rollback was no longer byte-exact.

Root cause

npm_bin::form_for_write and npm_bin::needs_fix took the package.json listing (is_listed_bin) as proof that npm rewrote the shebang, ignoring whether npm linked the bin at all.

Fix

is_listed_bin is now is_linked_bin: a listed bin counts as npm's form only when npm linked it:

  • a .bin/<name> entry in the node_modules the package sits in (where npm links it; a symlink must resolve to this file, and a cmd/shell shim must name this file's path below node_modules), under the names npm links it as (bin keys, unscoped package name, file names for arrays and directories.bin);
  • a global prefix's bin/<name> (Unix) or <name>.cmd (Windows);
  • for a package inside a virtual store (pnpm .pnpm or a relocated virtualStoreDir, npm linked .store, vlt, Bun, or a copy outside any node_modules such as Yarn's pnpmStoreFolder), whose bins are linked into each dependent's .bin, the listing alone still counts, as before.

The #1791 repair (a linked bin an older release left with a CRLF shebang) still works; the on-disk LF-shebang evidence path (#1756) is unchanged. Wrappers under npm/, pypi/, gem/ don't touch this logic.

Tests (red → green)

Issue shape Test Without fix With fix
#1804 --no-bin-links patch::apply::tests::apply_writes_recorded_bytes_for_bin_npm_did_not_link (apply, re-apply, needs_npm_bin_rewrite, rollback byte-exact) FAIL (LF shebang written) pass
#1804 directories.bin from file: same test, second case FAIL (old listing) pass
#1804 real npm, both shapes e2e_redirect_npm_build::npm_agent_keeps_crlf_shebang_of_bin_npm_did_not_link (npm 10.9.4) FAIL at apply pass
link detection npm_bin::tests::{is_linked_bin_wants_npms_link, is_linked_bin_follows_a_symlink_to_this_file, is_linked_bin_finds_global_and_pnpm_links, is_linked_bin_looks_only_where_npm_links_the_copy, is_linked_bin_wants_the_shim_to_run_this_file, is_linked_bin_takes_the_listing_in_relocated_stores, bin_names_are_npms_link_names} new pass

Existing #1791 unit tests now put the package under node_modules with a .bin link (the state npm leaves); the #1791/#1756 real-npm e2e tests pass unchanged.

Local runs:

  • cargo fmt --all -- --check: clean
  • cargo clippy --workspace --all-features -- -D warnings: clean
  • cargo test -p socket-patch-cli --all-features --test e2e_redirect_npm_build -- --ignored: 9 passed
  • cargo test --workspace --all-features --no-fail-fast: all pass except tests that can't pass in this sandbox: e2e_bun_lockb (2, need network to patches-api.socket.dev), and 9 permission-based tests (chmod-based write/remove failures; the sandbox runs as root). The 4 core ones fail identically on main here.

/code-review high

Fixed: nested copies were matched against the top-level .bin (now nearest node_modules only); virtual-store layouts (pnpm workspace members, npm linked, vlt, Bun) missed the link (now the listing counts there, with no store-wide scan); cheap disk probe now runs before the link lookup; trailing-slash bin keys; fixture for apply_keeps_crlf_shebang_npm_does_not_fix now has a .bin link so it still checks the listing.
Not changed:

Bugbot

Two findings on 871630f, both fixed in d2387bf (threads answered and resolved): a plain file that only had the bin's name (a project's own <bin>.cmd, or a lib workspace's sibling bin/<name>) counted as npm's link; and relocated stores lost the listing fallback. Bugbot's re-review of d2387bf: no issues found.

https://claude.ai/code/session_011f9aeQu3zbzV1egVhwbLdK


Note

Medium Risk
Changes npm bin shebang logic used on every patch apply/rollback, with filesystem heuristics for .bin links and shims; regression risk is mitigated by broad new tests but edge-case layouts could still misclassify linked vs unlinked bins.

Overview
Fixes #1804: patch apply and rollback no longer assume every package.json bin was rewritten by npm’s fixBin. npm only normalizes shebangs on bins it links; with --no-bin-links or directories.bin from a file: tarball install, on-disk bytes stay CRLF and must match the patch blobs exactly.

is_listed_bin is replaced by is_linked_bin, which checks for a real link (nearest node_modules/.bin, global prefix shims, symlink/canonicalize, shim path matching) and still treats virtual-store layouts (pnpm, relocated stores) like before when links live on dependents.

Apply/rollback write recorded afterHash/beforeHash for unlinked bins; linked-bin #1791 repair and LF-shebang heuristics are unchanged. Tests add unit coverage for link detection, apply+rollback byte-exact paths, refactored CRLF e2e helpers, and a real-npm e2e for both failure shapes.

Reviewed by Cursor Bugbot for commit 5789c73. Configure here.


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
Since #1792, apply and rollback treated every file a package lists as a
bin as one npm had rewritten to an LF shebang at install. npm only does
that for bins it links. With bin-links=false (--no-bin-links), or for a
directories.bin file of a package installed from a tarball or folder,
the installed file keeps its CRLF shebang, so apply wrote bytes that
matched neither the patch nor a fresh install, and rollback no longer
restored the installed file byte for byte.

A listed bin now counts as npm's form only when npm linked it: a
.bin/<name> entry in a node_modules the package sits under (a symlink
must point at this file), a pnpm dependent's .bin entry, or a global
prefix bin or .cmd shim. Bins npm did link, including ones an older
release left with a CRLF shebang (#1791), are still repaired.

Fixes #1804

Assisted-by: Claude Code:claude-opus-5-5
npm links a package's bins into the .bin folder of the node_modules the
package sits in, so a nested copy is no longer taken as linked because
the top-level copy of the same package has a .bin entry. Packages in a
virtual store (pnpm, npm's linked strategy, vlt, Bun) get their bins
linked into each dependent instead, so there the bin listing alone
still counts, as it did before.

Also check the on-disk shebang before the link, which is cheaper, and
read a bin key with a trailing slash the way npm does.

Refs #1804

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 29, 2026 02:38
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

1 similar comment
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

A regular file that merely had a bin's name, such as a project's own
<name>.cmd or a lib/../bin script, counted as npm's link, so a bin npm
never linked could still be rewritten with an LF shebang. A shim now
has to name this file's path below node_modules, as npm's and pnpm's
shims do; a symlink still has to resolve to it.

Copies in a store npm doesn't lay out (outside any node_modules, as in
Yarn's pnpmStoreFolder, or in a name@version entry of a relocated pnpm
virtualStoreDir) keep taking the bin listing as the link, as before.

Refs #1804

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 29, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Ready for review at d2387bf.

  • CI: 50/50 checks green (31 success, 19 skipped) on d2387bf; mergeable, no conflicts.
  • Bugbot: reviewed d2387bf — no new issues; both earlier threads resolved.
  • Reviewer focus: is_linked_bin in crates/socket-patch-core/src/patch/npm_bin.rs decides when CRLF→LF shebang rewriting applies (only for bins npm actually links into .bin / global shims).

Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Final review brief

What it does. Since #1792, apply/rollback treated every file a package lists as a bin as one npm had rewritten to an LF shebang. npm only does that for bins it actually links, so with --no-bin-links or a directories.bin file from a tarball/folder install, apply wrote bytes matching neither afterHash nor a fresh install. Now a listed bin counts as npm's form only when a .bin/global-prefix link (symlink to this file, or a shim naming it) exists; virtual stores (pnpm, npm linked, vlt, Bun, relocated stores) keep the old listing-only behaviour. Fixes #1804.

Risk: medium. It changes which bytes apply writes for npm bin files, and the link detection is heuristic over package-manager layouts. Every uncertain layout (no node_modules ancestor, any virtual store) falls back to the previous behaviour, so the change is limited to plain npm/yarn/bun trees, which the tests cover.

Look here

Verified

  • Read the full diff (4 files, +591/−105). Code only; CHANGELOG.md untouched; no debug leftovers.
  • cargo test -p socket-patch-core --lib -- npm_bin crlf bin on d2387bf: 188 passed.
  • Put npm_bin.rs back to main and ran apply_writes_recorded_bytes_for_bin_npm_did_not_link: it fails (apply.rs:4343), so the test really covers the fix.
  • CI on d2387bf: 31 success, 19 skipped, 0 failed (ci-ok and clippy green). Cursor Bugbot: success. Its 2 earlier findings were fixed and their threads resolved.

Changes I made: none.

Open questions (non-blocking)

  • shim_names matches by substring, so a shim for a-crlfbin/cli.js would also match crlfbin/cli.js. It only matters when the shim also carries this package's bin name, which is far-fetched and errs toward the previous behaviour.

Auto-merge (squash) is armed: approving sends this straight to the merge queue.


Generated by Claude Code

A shim for another package whose folder name ends the same way, such as
a-crlfbin/cli.js, or a sibling file like cli.json, counted as running
this package's bin. The shim now has to name the path as whole
segments.

Refs #1804

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[agent] Addressed the final review's open question in 5789c73: shim_names now matches the path below node_modules only as whole segments (a / before it, no path character after it), so ../a-crlfbin/cli.js and ../crlfbin/cli.json no longer count. Covered in is_linked_bin_wants_the_shim_to_run_this_file. Unit tests, clippy and the crlf e2e tests pass locally.


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 5789c73. Configure here.

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Burn-down agent: Ready for review confirmed at 5789c73 (the label was first applied at d2387bf).

  • CI: 50/50 check runs on 5789c73 green (31 success, 19 skipped); mergeable, no conflicts.
  • Bugbot: reviewed 5789c73, no new issues.
  • Since the label: 5789c73 makes shim_names match the path below node_modules only as whole segments, answering the final review's open question.

Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Final review brief (updated for 5789c73)

What it does. apply/rollback only treat an npm bin as LF-shebang-rewritten when npm actually linked it (a .bin entry or global-prefix shim that points at this file), not merely because package.json lists it. Fixes CRLF shebangs being rewritten under --no-bin-links and for directories.bin from file: installs (#1804). Since my last brief (d2387bf), 5789c73 makes a shim count only when it names the bin path as whole segments, so ../a-crlfbin/cli.js or cli.json no longer match.

Risk: medium. It changes which installed bins apply rewrites, so a missed link shape would bring back a byte mismatch on a linked bin. Virtual stores and copies outside node_modules keep the old listing-only behavior, which limits that.

Look here

Verified. Re-read the 5789c73 delta against npm cmd-shim and shell-shim output (quoted paths end in ", which isn't a path character, so real shims still match) and its new negative tests. CI 50/50 on 5789c73 (31 success, 19 skipped), Cursor Bugbot green, mergeable, no open review threads, CHANGELOG.md untouched.

Changes I made. None.

Auto-merge is armed, so approving sends this straight to the merge queue.


Generated by Claude Code

Merged via the queue into main with commit b73b080 Oct 29, 2026
50 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the agent/fix-npm-unlinked-bin-shebang branch October 29, 2026 13:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review

Projects

None yet

3 participants