Skip to content

npm apply exits 1 when every patch targets a platform-skipped optional dependency (fsevents, @esbuild/*), so the setup hook fails npm ci and npm install on other OSes #403

Description

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

Summary

socket-patch apply exits 1 when every manifest patch targets a package that npm skipped on purpose on this host, such as a platform-specific optionalDependencies entry like fsevents (macOS only) or @esbuild/<os>-<cpu>. The tree is already in its correct end state: the package is in the lock with optional: true and an os/cpu filter that excludes this host, so there's nothing to patch. But apply still prints Error: The targeted manifest patch matched no installed package and fails.

Because socket-patch setup wires apply --silent into postinstall and dependencies, this makes npm ci and npm install fail on every OS that doesn't install that package. A team that records a patch for fsevents on a Mac breaks its Linux CI. A team that patches @esbuild/linux-x64 in Linux CI breaks every macOS and Windows developer's npm install.

The result depends on what else is in the manifest. Add one patch for a package that is installed, and the same unmatched optional package downgrades to a warning with exit 0.

Impact

  • The installs fail (npm error command failed … socket-patch apply --silent --ecosystems npm, exit 1) on npm 8, 10, 11 and 12 as soon as the only patch in the manifest is for a platform-skipped optional dependency.
  • Platform-gated optional packages are where many native or binary CVEs live (fsevents, @esbuild/*, @rollup/rollup-*, @swc/core-*, @next/swc-*), so a cross-platform team hits this on its first such patch.
  • The released 4.0.0 and 3.3.0 behave the same way, so this isn't a regression.

Repro (Linux, main f6b7fb9)

mkdir app && cd app
echo '{"name":"app","version":"1.0.0","private":true,"dependencies":{"chokidar":"3.6.0"}}' > package.json
npm install                      # [email protected] is in the lock (optional, os: darwin) but not installed
# record a patch for pkg:npm/[email protected], as `socket-patch get`/`scan` on a Mac would; here
# it's hand-staged: .socket/manifest.json with package/fsevents.js before/after hashes + blobs
socket-patch setup --yes         # postinstall/dependencies: npx @socketsecurity/socket-patch apply --silent --ecosystems npm
rm -rf node_modules && npm ci
#   npm error command sh -c socket-patch apply --silent --ecosystems npm
#   npm error Error: The targeted manifest patch matched no installed package:
#   npm error   - pkg:npm/[email protected]
#   exit 1
socket-patch apply --offline --json   # status partialFailure, events [skipped/package_not_installed], exit 1

With a second patch for an installed package (e.g. is-glob) in the same manifest, apply --silent patches it and exits 0: the fsevents entry becomes a warning.

Expected vs actual

Expected: a manifest entry whose package the project lock resolves, but which npm deliberately didn't install on this host (an optional entry whose os/cpu/libc excludes it, or more generally any lockfile-only entry), is a calm skip (skipped / package_not_installed) that never fails the run. That's exactly what CLI_CONTRACT.md promises for the same situation in scan --apply: it "partitions lockfile-only patches out BEFORE download (calm skipped/package_not_installed records — never an error exit …)". rollback already treats a not-installed package as satisfying its end state (CLI_CONTRACT.md, rollback results). The install hook that setup wires should never fail an install whose tree is correct.

Actual: apply (the command the setup hook runs) exits 1 with an error whenever no targeted patch matched an installed package, regardless of whether the lock shows the package was skipped on purpose.

OS npm only-other-platform patches: apply / npm ci with the setup hook host + other-platform patches: apply
Linux 8.19.4 exit 1 / exit 1 (and npm install exit 1) —
Linux 10.9.7 exit 1 / exit 1 (and npm install exit 1) —
Linux 11.20.0 exit 1 / exit 1 (and npm install exit 1) exit 0, warning only
Linux 12.1.0 exit 1 / exit 1 (and npm install exit 1) —
Linux (ubuntu-latest) 10.9.7 (probe) exit 1 / exit 1 exit 0, host package patched
Linux (ubuntu-latest) 12.1.0 (probe) exit 1 / exit 1 exit 0, host package patched
macOS (macos-latest, arm64) 10.9.7 (probe) exit 1 / exit 1 exit 0, host package patched
macOS (macos-latest, arm64) 12.1.0 (probe) exit 1 / exit 1 exit 0, host package patched
Windows (windows-latest) 10.9.7 (probe) exit 1 / exit 1 exit 0, host package patched
Windows (windows-latest) 12.1.0 (probe) exit 1 / exit 1 exit 0, host package patched

The local Linux cells use [email protected] via [email protected], with the main build in the hook. The probe cells use [email protected] with patches for the two @esbuild/* platform packages the runner doesn't install, and the host's own package in column 3. The released 4.0.0 and 3.3.0 also exit 1 on the Linux fsevents repro.

Suspect code

  • crates/socket-patch-cli/src/commands/apply.rs:2176-2181: none_matched (no targeted purl matched an installed package) sets has_errors = true, without consulting the lockfile.
  • crates/socket-patch-cli/src/commands/apply.rs:1785-1810: the same decision on the empty-tree path (success: unmatched.is_empty()).
  • For comparison, scan --apply's lockfile-only partition (CLI_CONTRACT.md "Lockfile supplement (v3.4)") already knows these packages are lockfile-resolved, and a calm skip there doesn't change the exit code.

Probe run: https://github.com/SocketDev/socket-patch/actions/runs/36796693378 (all 6 jobs: RESULT only-other … apply_silent_exit=1 … npm_ci_exit=1, RESULT host-plus-other … apply_silent_exit=0 host_patched=1).

Activity

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