Skip to content

Path-scoped agent scan packages/a and rollback packages/a skip a workspace member's npm alias (lp@npm:left-pad) under install-strategy=linked, while its plain links are in scope (exit 0, success) #1266

Description

[agent] Found by the scheduled npm bug-hunt routine (ledger #302).

Summary

The project is an npm workspace using install-strategy=linked (npm ≥ 9.4). Each member's dependencies are symlinks into the root node_modules/.store. For a member whose own dependencies are "ms": "2.1.2" and an alias "lp": "npm:left-pad@1.3.0":

  • scan --mode agent packages/a scans 1 package (ms@2.1.2) and patches it. It never selects left-pad@1.3.0, so packages/a/node_modules/lp (what require('lp') in member a loads) stays unpatched. The status is success, exit 0, with no warning.
  • rollback packages/a restores ms but not left-pad. packages/a/node_modules/lp keeps the Socket patch, and the status is again success, exit 0.

The unscoped scan --mode agent does find and patch the alias copy (#852 / #356 fixed). Under the default hoisted layout, where the member's alias is a real nested directory, the scoped scan also finds it. Only the link form of a member's alias falls outside the path scope, while the member's plain ms link is inside it.

Impact

  • A user who scopes the agent scan to one member believes that member's dependencies were scanned. A vulnerable package it loads through an alias is silently left unpatched.
  • rollback <member-path> reports success but leaves patched bytes in the member's tree. The run is no longer symmetric with the scan that wrote them.

Repro (Linux, main a80b89e; reproduced on npm 10.9.4 and npm 12.2.0 / Node 24, twice each)

I used a local patch-API mock (public proxy routes, SOCKET_PROXY_URL / SOCKET_API_URL pointing at it) that serves free patches for ms@2.1.2 and left-pad@1.3.0. Each patch appends // SOCKET-PATCHED to index.js.

mkdir -p ws/packages/a && cd ws
echo '{"name":"root","version":"1.0.0","workspaces":["packages/*"]}' > package.json
echo '{"name":"a","version":"1.0.0","dependencies":{"lp":"npm:left-pad@1.3.0","ms":"2.1.2"}}' > packages/a/package.json
printf 'install-strategy=linked\n' > .npmrc
npm install
ls -l packages/a/node_modules
#  lp -> ../../../node_modules/.store/left-pad@1.3.0-…/node_modules/left-pad   (npm 10: .store/lp@1.3.0-…/node_modules/lp)
#  ms -> ../../../node_modules/.store/ms@2.1.2-…/node_modules/ms

socket-patch scan --mode agent packages/a --json
#  status success, scannedPackages 1, packages: [pkg:npm/ms@2.1.2]
tail -n1 packages/a/node_modules/ms/index.js   # // SOCKET-PATCHED
tail -n1 packages/a/node_modules/lp/index.js   # }      <- unpatched

socket-patch scan --mode agent --json           # unscoped control
#  packages: [pkg:npm/left-pad@1.3.0, pkg:npm/ms@2.1.2]; lp now // SOCKET-PATCHED

socket-patch rollback packages/a --json
#  status success, warnings: [out_of_scope_copies_restored]
tail -n1 packages/a/node_modules/ms/index.js   # }      (restored)
tail -n1 packages/a/node_modules/lp/index.js   # // SOCKET-PATCHED   <- left patched

Hoisted control: with the root depending on "lp": "npm:left-pad@1.1.3", member a's lp@1.3.0 is nested as a real directory. There, scan --mode agent packages/a selects both left-pad@1.3.0 and ms@2.1.2 (npm 10.9.4).

Expected vs actual

CLI_CONTRACT.md, "Path-scoped scans": "a package is in scope iff ANY of its crawled installed copies sits under a matching path (every copy, enumerated the way rollback's path targets enumerate them: a pnpm workspace member's link into the root .pnpm store and a nested member copy count, not just the one copy the crawl records per purl)". packages/a/node_modules/lp is member a's link into the root .store for pkg:npm/left-pad@1.3.0, just as packages/a/node_modules/ms is for ms. So left-pad@1.3.0 should be in scope for scan packages/a and selected by rollback packages/a.

Actual: the ms link counts, the lp link does not, and both commands exit 0 with success.

OS × version

npm 10.9.4 (Node 22) npm 12.2.0 (Node 24)
Linux, linked + member alias reproduces ×2 reproduces ×2
Linux, hoisted (nested real alias dir) passes not run
macOS / Windows not probed (probe branches are blocked for this routine; the logic is OS-independent)

I didn't bisect it. The rollback half predates #1007 (#778 only added the scan-side re-enumeration via find_all_packages_for_rollback_reusing, which inherits the gap).

Suspect code

  • crates/socket-patch-core/src/crawlers/npm_crawler.rs:1752 (alias_copies) counts only real package directories ("links are dependency edges into a store … never copies of their own"), so a member's alias link is never returned for the member's node_modules root.
  • The direct probe a few lines above (nm_path.join(&target.dir_key), around line 1694) does follow a link whose name equals the package name. That's why the member's ms link is in scope.
  • That rule is right for apply's physical copies, because the store entry is patched once. But the path-scope enumeration in find_all_packages_for_rollback_reusing (crates/socket-patch-cli/src/ecosystem_dispatch.rs:488) uses the same results to decide which member paths a package is reachable from. It needs the alias link paths too, the same way it gets the plain-name links.
  • pnpm members link aliases into .pnpm the same way. That likely has the same gap, but I haven't tested it.

Activity

  1. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged: priority:p1 (npm). Not a duplicate and no open PR covers it; distinct cause from other open npm issues (path-scope enumeration misses a linked-strategy member's alias symlink).


    Generated by Claude Code

  2. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    v5 triage: P2, not a release blocker. Retain agent path-scoped npm alias handling with the linked install strategy at P2. This combined nondefault layout does not gate ordinary lockfile patching.

    This follows the maintainer's release scope: one normally completing CLI instance, prioritizing valid-lockfile patch/install behavior, compatibility, and actionable CLI UX.

  3. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] pnpm cells from the scheduled pnpm bug-hunt routine (ledger #303), on main 9ab72d4 (5.0.0). The same defect affects pnpm's default isolated linker, not just npm's install-strategy=linked. So on pnpm this isn't a non-default layout.

    Repro (Linux, Node 22, offline mock of the patch API serving one left-pad@1.3.0 patch):

    mkdir -p ws/packages/a && cd ws
    echo '{"name":"root","private":true}' > package.json
    printf 'packages:\n  - packages/*\n' > pnpm-workspace.yaml
    echo '{"name":"a","version":"1.0.0","dependencies":{"lp":"npm:left-pad@1.3.0","is-number":"7.0.0"}}' > packages/a/package.json
    pnpm install            # packages/a/node_modules/lp -> ../../../node_modules/.pnpm/left-pad@1.3.0/node_modules/left-pad
    socket-patch scan --mode agent packages/a --json   # scannedPackages: 1, applied: 0 (left-pad not in scope)
    socket-patch scan --mode agent --json              # unscoped: scannedPackages: 2, applied: 1
    socket-patch rollback packages/a --json            # exit 1, path_glob_no_match; lp stays patched
    pnpm scoped scan packages/a selects the alias rollback packages/a restores it control: plain "left-pad": "1.3.0" in the member
    9.15.9 no (exit 0, success, 1 package scanned) no (exit 1, path_glob_no_match) —
    10.34.6 no no —
    12.11.2 no no selected and patched

    Each cell was run twice. The unscoped rollback restores the alias copy correctly.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions