You will end up with a dependency patch registered in npm's own patchedDependencies, hash-verified in a lockfileVersion: 4 lockfile, applying during npm ci --ignore-scripts, and one fewer dev dependency in the tree. Getting there starts with an error message patch-package never prints.

Short version: npx patch-package <pkg> on npm 12 exits with status: 1, signal: null, output: [null, null, null] and no text. The hidden failure is EALLOWREMOTE from the temporary npm i --force that patch-package runs to fetch a clean copy of the package, because npm 12 defaults allow-remote to none and patch-package passes an absolute tarball URL. One env var proves the diagnosis, and the durable fix is to stop creating patches with patch-package and register them with npm patch instead. The same default flip is behind the EALLOWREMOTE errors you get straight from npm.

Two facts make this worth writing down. patch-package issue #615, "No more working under npm 12", filed 2 August 2026 against patch-package 8.0.1, is still open as of 5 October 2026, and the proposed fix (PR #616, which passes --allow-remote=all when npm is 12 or newer) is unmerged, so no released patch-package version works. An assistant trained before the npm 12 default flip will send you into a diff-debugging session instead. Meanwhile the replacement landed quietly: native dependency patching (npm patch add/commit/update/ls/rm) merged in npm/cli PR #9439 on 18 June 2026 and ships in the npm 12 line, so the migration target is already on your machine.

Settle one thing before you start debugging the wrong half of the pipeline: applying existing patches still works. The apply path reads patches/*.patch and writes files without ever invoking npm, and the root package's own postinstall is not blocked by npm 12's allowScripts gate (only dependency scripts are). CI stays green, which is why this lands as one developer stuck on a Tuesday rather than a red build. If you are still mapping out what npm 12 does break, the npm 12 rollout notes cover the rest.

npx patch-package pkg exits 1status: 1, output: [null, null, null]Retry withnpm_config_allow_remote=allsucceeds?EALLOWREMOTE on the temp installnpm 12 default allow-remote=noneReal diff failurekeep the temp dir and read itRegister the patch withnpm patch add and npm patch commitDrop the patch-package postinstall"yes""no"

Prerequisites

  • npm 12.0.0 or newer (npm --version). The native patch commands are documented in the v12 CLI docs for npm patch; if npm patch ls exits with an unknown-command error, the npm on your PATH predates the feature.
  • Node.js 24 or 26. Issue #615 reproduces on Node v24.18.0, and npm/cli issue #9738 was filed against Node 26.5.0.
  • A repository that currently uses patch-package, with a patches/ directory and "postinstall": "patch-package" in package.json.
  • jq for reading package-lock.json, and write access to commit a changed lockfile.
  • One caveat up front: patches require lockfileVersion: 4. The PR description gives the reason plainly, which is that older npm clients abort rather than silently installing unpatched code. Confirm every CI image and every developer is on npm 12 before you commit the result.

Step-by-step

1. Get the real error out of patch-package

patch-package runs the clean-copy install through spawnSafeSync with stdio captured, so a failing child process surfaces as the raw result object with three nulls in it. Reproduce the child command yourself rather than fighting the wrapper. First pull the resolved URL for the package you are patching:

jq -r '.packages | to_entries[]
  | select(.key == "node_modules/left-pad")
  | .value.resolved' package-lock.json

Then run the two installs side by side in a throwaway directory:

mkdir -p /tmp/pp-repro && cd /tmp/pp-repro
npm init -y >/dev/null
npm i --force left-pad@1.3.0
npm i --force "left-pad@https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz"

Same package, same registry, same bytes. The version specifier installs. The absolute tarball URL fails:

npm error code EALLOWREMOTE
npm error Fetching packages of type "remote" have been disabled

patch-package's makePatch path hands npm an absolute URL at src/makePatch.ts lines 234 and 242, and npm 12 refuses it by default.

2. Confirm it in one line, without touching your config

npm_config_allow_remote=all npx patch-package left-pad

A patch file appearing under patches/ tells you the failure was the fetch and not your diff. Keep this as a diagnostic. The variable applies to every install in that process tree, and disabling a supply-chain control to create a patch is a poor trade now that npm patches natively. When allow-remote genuinely needs widening, and why replace-registry-host usually beats allow-remote=all for a private registry, is covered in the EALLOWREMOTE piece linked above.

3. Extract the package for editing with npm

npm patch add left-pad@1.3.0

npm patch add extracts the registry tarball into a temporary edit directory and prints the path. Pass --edit-dir ./edit/left-pad to choose it yourself, which makes the next command scriptable. --ignore-existing discards an abandoned session from an earlier attempt.

4. Make the change, then commit the diff

Edit files inside the edit directory, not inside node_modules:

$EDITOR ./edit/left-pad/index.js
npm patch commit ./edit/left-pad

npm patch commit diffs the edited tree against a clean copy of the original tarball, writes a unified diff to patches/<name>@<version>.patch, adds the entry to patchedDependencies in the root package.json, and records a content hash in package-lock.json. Add --keep-edit-dir if you expect to iterate.

5. Port the patches you already have

npm's default patch directory is also patches/, but the filename conventions differ: patch-package writes left-pad+1.3.0.patch, npm writes left-pad@1.3.0.patch. The two formats coexist in one folder and neither tool reads the other's files. Re-create each patch through npm patch add and npm patch commit rather than renaming a diff, because the hash in the lockfile comes from npm's own extraction.

6. Remove the patch-package hook

Leave the old postinstall in place and both mechanisms write the same file; the second apply then hits content it did not expect.

npm pkg delete scripts.postinstall
npm uninstall patch-package
git rm patches/left-pad+1.3.0.patch

Verify it works

Reinstall from scratch and read the lockfile rather than trusting the install log:

rm -rf node_modules
npm ci
jq '.lockfileVersion' package-lock.json
jq '.packages["node_modules/left-pad"].patched' package-lock.json
npm patch ls

Expect 4 from the first jq, a patched object carrying integrity and path from the second, and one row per patch from npm patch ls with the number of installed nodes it matched. A match count of 0 means the patch is registered and applying to nothing.

The check that separates the new mechanism from the old one is the scriptless install:

rm -rf node_modules
npm ci --ignore-scripts
grep -c "YOUR_SENTINEL_STRING" node_modules/left-pad/index.js

Under patch-package that grep returns 0, because the apply rode on a lifecycle script. Under patchedDependencies it returns your count. The npm/cli PR is explicit that patches are applied during the install, so they hold across --ignore-scripts, every install-strategy, and transitive placement.

Common pitfalls

EPATCHNONREGISTRY. Patches need a stable registry tarball as a baseline. A dependency reached through a non-registry consumer edge (file:, git:, http(s):) is rejected with this code. A workspace package that depends on a git fork puts everything underneath that fork out of reach, so publish the fork to your private registry first.

Third-party lockfile rewriters do not know about lockfileVersion: 4. socket-patch issue #711, opened 3 October 2026 and reproduced on npm 12.1.0 and 12.2.0, is the live example. Its vendored path refuses the lock with lockfileVersion Some(4); only v2/v3 locks … and advises running "npm install with npm >= 7 to upgrade it", which cannot work because npm 12 is what wrote the v4 lock. The hosted path preserves npm's patched object while swapping in its own tarball URL, and overlapping diffs then fail npm ci with EPATCHFAILED. Decide which tool owns a given package's bytes and keep the other away from it.

--allow-unused-patches and --ignore-patch-failures are not CI flags. They let an install succeed when a patch matched nothing or failed to apply, which in a pipeline turns a loud failure into a silently unpatched artifact. That is the same shape as npm ci exiting 0 while build/Release is missing.

An approved install script is a different failure. If the blocked thing is a dependency's postinstall rather than a fetch, you are in allowScripts territory and the error text says so. npm/cli issue #9738, open since 9 July 2026, documents the sharp edge: install a package from a remote tarball URL and its allowScripts entry is not honoured at all, producing npm warn install-scripts 1 package had install scripts blocked because they are not covered by allowScripts followed by ENOMATCH from npm approve-scripts. The schema for that field is a package.json key and not a .npmrc setting, which the piece on npm approve-scripts versus --allow-scripts works through.

npm patch is npm-only. A monorepo that installs with pnpm or Yarn in some jobs and npm in others cannot share patchedDependencies. Pick one installer per lockfile before you migrate.

Wrap-up

The patch now lives as a unified diff under patches/, registered in patchedDependencies, hash-verified in a v4 lockfile, and applied by the installer instead of by a lifecycle script. Add one CI guard on top of it: fail the build when npm patch ls reports a zero match count, which catches the day a dependency bump moves past the version your patch targets. Rebase it with npm patch update <pkg> --to <new-version>.