Skip to content

Bun isolated linker: transitive packages under node_modules/.bun are "not installed" in agent mode, and scan --mode agent exits 0 with them unpatched #366

Description

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

Summary

With Bun's isolated linker, every package lives at node_modules/.bun/<name>@<version>/node_modules/<name>. The importer's node_modules only holds symlinks to its direct deps. Agent mode never looks inside node_modules/.bun, so a transitive dependency is treated as lockfile-only. scan --mode agent reports it skipped / package_not_installed, doesn't record it in the manifest, and exits 0 with status: "success". The installed copy stays unpatched.

The isolated linker is Bun's default for new workspaces from Bun 1.3.x on (configVersion: 1 locks). On a fresh Bun 1.3.14 or 1.4.2 workspace with no bunfig.toml, agent mode doesn't patch any transitive dependency. It's also opt-in for any project through [install] linker = "isolated".

This is the Bun twin of #359 (npm install-strategy=linked / .store) and #362 (pnpm custom virtual store). The code path is different: the .bun store directory.

Impact

  • The vulnerable transitive copy stays on disk while scan --mode agent --json --yes exits 0 with success. The patch isn't even written to .socket/manifest.json, so a later apply or the postinstall hook never retries it.
  • get <uuid> and apply on such a purl fail loudly (partial_failure, exit 1, package_not_installed), which is at least honest. The silent path is scan.
  • VEX correctly omits the package (package_not_found), so there's no false attestation.

Repro (Linux, bun 1.4.2, main f6b7fb9)

mkdir iso && cd iso
printf '{"name":"app","version":"1.0.0","dependencies":{"mkdirp":"0.5.6","left-pad":"1.3.0"}}\n' > package.json
printf '[install]\nlinker = "isolated"\n' > bunfig.toml
bun install
ls node_modules/.bun            # left-pad@1.3.0  minimist@1.2.8  mkdirp@0.5.6  node_modules
# patch API mock serving free patches for left-pad@1.3.0 and minimist@1.2.8 (the probe below embeds one)
socket-patch scan --mode agent --json --yes --api-url http://127.0.0.1:18901 --org test-org --api-token fake
echo $?                          # 0
head -c 22 node_modules/.bun/minimist@1.2.8/node_modules/minimist/index.js   # unpatched

Envelope excerpt: "status": "success", "lockfileOnlyPackages": 1, apply.patches: left-pad added (the direct symlink works), minimist skipped package_not_installed. node -e 'require.resolve("minimist",{paths:[dirname(require.resolve("mkdirp"))]})' resolves to the .bun/minimist@1.2.8/... copy, so the package is installed and in use.

The default-linker workspace shape reproduces the same way with no bunfig: a root {"workspaces":["packages/*"]} plus packages/a depending on mkdirp@0.5.6, on bun 1.3.14 / 1.4.2.

Control: the same project with linker = "hoisted" patches both packages.

Expected vs actual

  • Expected: CLI_CONTRACT.md "Deeply nested transitive dependencies are fully supported", which says apply "patches a package by PURL … regardless of how deep in the dependency tree it was installed". The crawler already walks pnpm's .pnpm, legacy .<registry> stores and vlt's .vlt store for exactly this transitive-only-home layout. pkg_managers.rs even documents that bun's isolated linker "populates node_modules/.bun/".
  • Actual: .bun falls through to the generic hidden-entry skip, so its entries are never probed or crawled.

OS × version

OS Bun 1.2.23 Bun 1.3.14 Bun 1.4.2
Linux (sandbox + ubuntu-latest) fail (bunfig isolated); default workspace is hoisted, pass fail (bunfig isolated and default workspace) fail (both)
macOS (macos-latest) fail (bunfig isolated); default workspace hoisted, pass fail (both) fail (both)
Windows (windows-latest) fail (bunfig isolated) fail (bunfig isolated); default-workspace cell inconclusive (bun install exited 1 on the runner) same as 1.3.14

Releases: it's the same on 4.0.0 and 3.3.0 (npm @socketsecurity/socket-patch), so it isn't a regression.

Suspect code

  • crates/socket-patch-core/src/crawlers/npm_crawler.rs:1069: the nested-walk hidden-entry skip used by find_by_purls (apply / rollback / get). .pnpm, .vlt and legacy pnpm stores are special-cased just above it; .bun is not.
  • crates/socket-patch-core/src/crawlers/npm_crawler.rs:1392: the same skip in the crawl_all walk (scan), which is why the package shows up only through the lockfile supplement (lockfileOnlyPackages).
  • crates/socket-patch-core/src/crawlers/pkg_managers.rs:93: the detector knows about .bun/ but only uses it for classification.

Probe run (3 OS × bun 1.2.23 / 1.3.14 / 1.4.2, main built on each runner): https://github.com/SocketDev/socket-patch/actions/runs/36765789215

Activity

  1. mikolalysenko commented on Sep 30, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged: priority:p1 (Bun, npm family). Not a duplicate.

    Shares root cause with #359, #362 and #373: the npm crawler recognizes dependency stores only by hard-coded directory names (.pnpm, .vlt, legacy .<registry>). Every other hidden dir, including .bun, falls into the generic hidden-entry skip in the same three walks: gather_node_modules (scan), nested_node_modules_of (apply/rollback) and find_store_peer_variant_copies. Will be fixed together.

    #359 and #362 are being fixed in the in-flight agent PR #365, which touches exactly those three walks. .bun isn't in that PR's scope yet. It should be added there when that PR is next taken over, or right after it lands, so the two changes don't conflict.


    Generated by Claude Code

  2. mikolalysenko commented on Oct 1, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Re-triage from the Bun bug-hunt routine (ledger #306): this still reproduces on main 61cfb9b, which is the merge of #365. #365 added the npm .store and pnpm virtualStoreDir walks but not .bun.

    Bun 1.4.2 on Linux, with a fresh workspace (packages/a depends on is-odd@3.0.1 → is-number@6.0.0) and the default isolated linker. Mock patches for both packages. Command: scan --mode agent --apply --yes --json:

    • exit 0, status: success
    • is-odd is patched, because it's reachable through the packages/a/node_modules symlink
    • pkg:npm/is-number@6.0.0 → notInstalled: true, apply skipped / package_not_installed, lockfileOnlyPackages: 1
    • node_modules/.bun/is-number@6.0.0/node_modules/is-number/index.js is unchanged

    So .bun still needs adding to the three walks that #365 touched.


    Generated by Claude Code

  3. mikolalysenko commented on Oct 1, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (with #373, #405, #495; shared root cause: the npm crawler only recognizes isolated stores by hard-coded names/shapes, so .bun, .deno and Yarn 4's .store are skipped). Branch: agent/fix-npm-crawler-isolated-stores. Claim-ID: 2026-10-01T19:20:55Z-253dcb


    Generated by Claude Code

  4. mikolalysenko commented on Oct 1, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft fix PR: #496


    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