TL;DR
patch-package is the original tool for patching npm dependencies — edit files in node_modules/, run npx patch-package <pkg>, and it saves a .patch file that auto-applies on install. pnpm patch is pnpm's built-in patching — same workflow, integrated into the package manager, stored in patches/. yarn patch is Yarn Berry's equivalent — yarn patch <pkg> opens an editable copy, yarn patch-commit saves the diff. In 2026: use your package manager's built-in patching (pnpm patch or yarn patch) if possible, fall back to patch-package for npm.
Key Takeaways
- patch-package: ~3M weekly downloads — works with npm, yarn classic, pnpm, package-manager-agnostic
- pnpm patch: built into pnpm — zero extra dependencies,
pnpm patch/pnpm patch-commit - yarn patch: built into Yarn Berry (v2+) —
yarn patch/yarn patch-commit - Patches are git-trackable
.patchfiles — your team gets the fix automatically - All three auto-apply patches on install —
postinstallhook or built-in integration - Always file an upstream PR — patches are temporary fixes, not permanent solutions
When to Patch Dependencies
Patch when:
✅ Critical bug fix — upstream hasn't released yet
✅ TypeScript type errors — @types package has wrong types
✅ Security vulnerability — can't upgrade (breaking changes), patch the specific issue
✅ Framework compatibility — small adjustment for your specific use case
Don't patch when:
❌ Feature additions — fork the package instead
❌ Major changes — too hard to maintain across version updates
❌ Upstream fix is available — just upgrade
Always:
1. File an issue/PR upstream
2. Add a comment in the patch explaining WHY
3. Check on every dependency upgrade if the patch is still needed
patch-package
patch-package — package-manager-agnostic:
Setup
npm install -D patch-package
# Add postinstall hook to auto-apply patches:
# package.json:
{
"scripts": {
"postinstall": "patch-package"
}
}
Create a patch
# 1. Edit the file directly in node_modules:
# Fix the bug in node_modules/some-package/dist/index.js
# 2. Create the patch:
npx patch-package some-package
# Creates: patches/some-package+1.2.3.patch
# This is a standard unified diff file
# 3. Commit the patch:
git add patches/
git commit -m "fix: patch some-package to fix XYZ bug"
Example: fixing a TypeScript type
# Problem: @types/express has wrong types for req.query
# Edit: node_modules/@types/express/index.d.ts
# Create scoped package patch:
npx patch-package @types/express
# Creates: patches/@types+express+4.17.21.patch
Example patch file
# patches/some-package+1.2.3.patch
diff --git a/node_modules/some-package/dist/index.js b/node_modules/some-package/dist/index.js
index abc1234..def5678 100644
--- a/node_modules/some-package/dist/index.js
+++ b/node_modules/some-package/dist/index.js
@@ -42,7 +42,7 @@ function processData(input) {
- if (input.length = 0) {
+ if (input.length === 0) {
return [];
}
Version-specific patches
# Patch applies to exact version by default:
# patches/react-dom+18.2.0.patch → only applies to 18.2.0
# When you upgrade react-dom to 18.3.0:
# patch-package will WARN if the patch doesn't apply cleanly
# → Review and recreate the patch if needed
pnpm patch
pnpm patch — built-in pnpm patching:
Workflow
# 1. Start patching — pnpm creates an editable copy:
pnpm patch some-package@1.2.3
# Output:
# You can now edit the following folder: /tmp/.pnpm-patch-xxx/some-package
# 2. Edit the file in the temporary folder:
# Edit /tmp/.pnpm-patch-xxx/some-package/dist/index.js
# 3. Commit the patch:
pnpm patch-commit /tmp/.pnpm-patch-xxx/some-package
# Creates: patches/some-package@1.2.3.patch
# Updates package.json:
package.json integration
{
"pnpm": {
"patchedDependencies": {
"some-package@1.2.3": "patches/some-package@1.2.3.patch"
}
}
}
Auto-application
# Patches apply automatically on pnpm install:
pnpm install
# → Applying patches...
# → Patched some-package@1.2.3
# No postinstall script needed — pnpm handles it natively
Removing a patch
# Remove from patchedDependencies in package.json
# Delete the patch file:
rm patches/some-package@1.2.3.patch
# Reinstall:
pnpm install
yarn patch
yarn patch — Yarn Berry (v2+) built-in:
Workflow
# 1. Start patching:
yarn patch some-package
# Output:
# ➤ YN0000: Package some-package@npm:1.2.3 got extracted to /tmp/xfs-xxx
# 2. Edit files in the extracted folder:
# Edit /tmp/xfs-xxx/dist/index.js
# 3. Save the patch:
yarn patch-commit -s /tmp/xfs-xxx
# Creates: .yarn/patches/some-package-npm-1.2.3-xxxxx.patch
# Updates package.json:
package.json integration
{
"resolutions": {
"some-package@1.2.3": "patch:some-package@npm%3A1.2.3#~/.yarn/patches/some-package-npm-1.2.3-xxxxx.patch"
}
}
Auto-application
# Patches apply automatically on yarn install:
yarn install
# → some-package patched
# No postinstall hook needed
Best Practices
Always document your patches
# In your codebase, create a PATCHES.md or add comments:
# patches/some-package+1.2.3.patch
# WHY: Fixes null reference when input is empty array
# UPSTREAM: https://github.com/owner/some-package/issues/123
# PR: https://github.com/owner/some-package/pull/456
# REMOVE WHEN: some-package >= 1.2.4 is released
CI verification
// package.json (patch-package):
{
"scripts": {
"postinstall": "patch-package",
"verify-patches": "patch-package --error-on-fail"
}
}
# GitHub Actions:
- name: Install dependencies
run: npm ci
# postinstall runs patch-package automatically
- name: Verify patches applied
run: npx patch-package --error-on-fail
# Fails CI if any patch doesn't apply cleanly
Monorepo patches
# patch-package in monorepo (from root):
npx patch-package some-package --patch-dir patches/
# pnpm workspace — patches defined in root package.json:
{
"pnpm": {
"patchedDependencies": {
"some-package@1.2.3": "patches/some-package@1.2.3.patch"
}
}
}
Feature Comparison
| Feature | patch-package | pnpm patch | yarn patch |
|---|---|---|---|
| Package manager | npm, yarn, pnpm | pnpm only | Yarn Berry only |
| Extra dependency | Yes | No (built-in) | No (built-in) |
| Workflow | Edit node_modules → patch | pnpm patch → edit copy → commit | yarn patch → edit copy → commit |
| Auto-apply | postinstall hook | Built-in | Built-in |
| Config location | patches/ dir | package.json patchedDependencies | package.json resolutions |
| Scoped packages | ✅ | ✅ | ✅ |
| Monorepo | ✅ | ✅ | ✅ |
| Patch on fail | --error-on-fail | Built-in error | Built-in error |
| Weekly downloads | ~3M | built-in | built-in |
When to Use Each
Use patch-package if:
- Using npm as your package manager (npm has no built-in patching)
- Need a tool that works across npm, yarn classic, and pnpm
- Team has mixed package manager usage
Use pnpm patch if:
- Already using pnpm — zero extra dependencies, native integration
- pnpm's
patchedDependenciesconfig is clean and declarative - Patches apply automatically without postinstall hooks
Use yarn patch if:
- Using Yarn Berry (v2+) — built-in, native workflow
- Yarn's resolution protocol handles patching transparently
General advice:
- Always file upstream PRs — patches are temporary
- Add comments explaining why each patch exists
- Review patches on every dependency upgrade
- Keep patches small — if you need major changes, fork instead
Real-World Patching Scenarios in 2026
Understanding when and how teams actually patch dependencies clarifies which tool fits your workflow.
Scenario 1: TypeScript type error in a published package
The most common patching scenario — a package ships incorrect types that cause TypeScript errors in your project, but the runtime behavior is correct. You cannot upgrade because no fix is released yet.
# Using pnpm patch (if on pnpm):
pnpm patch some-package@2.1.0
# Edit the .d.ts file in the temp directory
# For example, fix: null | undefined → undefined only
pnpm patch-commit /tmp/.pnpm-patch-xxx/some-package
# → Creates patches/some-package@2.1.0.patch
# → Updates package.json patchedDependencies
A good patch header comment that will save future you time:
# patches/some-package@2.1.0.patch
# WHY: TypeScript 5.5 strict mode flags null return on processItems()
# Upstream fixed in v2.1.1 (not yet released as of 2026-02-14)
# ISSUE: https://github.com/owner/some-package/issues/789
# REMOVE WHEN: version >= 2.1.1
Scenario 2: Security vulnerability with no fix available
A CVE is published for a transitive dependency. Upgrading the parent would require major refactoring. Patching the vulnerable line buys time until a proper fix is available.
# Check the specific vulnerable code
cat node_modules/vulnerable-pkg/src/parser.js | grep -n "eval("
# patch-package approach (works with any package manager):
# 1. Edit the file in node_modules to remove/neutralize the vulnerability
# 2. npx patch-package vulnerable-pkg
# 3. git add patches/ && git commit -m "security: patch CVE-2025-XXXX in vulnerable-pkg"
// Document in your security tracking:
{
"scripts": {
"postinstall": "patch-package",
"audit:patches": "echo 'Patches: patches/vulnerable-pkg+1.0.0.patch (CVE-2025-XXXX, remove when >= 1.0.1)'"
}
}
Scenario 3: Bug in a Vite plugin that only affects your specific setup
Framework-specific tooling is often patched to fix issues that only manifest in edge case configurations. The upstream maintainer is responsive but their release cycle is slow.
# Quick turnaround with pnpm patch:
pnpm patch vite-plugin-broken@3.2.1
# Fix the specific method that causes issues with your config
pnpm patch-commit /tmp/.pnpm-patch-xxx/vite-plugin-broken
Community Adoption in 2026
| Tool | Weekly Downloads | GitHub Stars | Package Manager |
|---|---|---|---|
| patch-package | ~3M | 10,000+ | npm, yarn classic, pnpm |
| pnpm patch | Built into pnpm | (part of pnpm: 30K stars) | pnpm only |
| yarn patch | Built into Yarn Berry | (part of Yarn: 41K stars) | Yarn Berry only |
patch-package's 10K GitHub stars and 3M weekly downloads reflect its status as the canonical solution for this problem over many years. Most online tutorials, Stack Overflow answers, and blog posts about patching npm dependencies reference patch-package — this documentation lead is self-reinforcing.
The "built-in" tools (pnpm patch, yarn patch) do not show separately in npm download stats, but they are increasingly the default choice for new projects. In 2026, if you are starting a new project on pnpm, there is no reason to add patch-package as an extra dependency — pnpm patch does the same thing with zero setup.
Migration Guide
From patch-package to pnpm patch:
# Existing patches in patches/ dir created by patch-package
# Convert to pnpm's format:
# 1. Apply the existing patch manually to node_modules:
git apply patches/some-package+1.2.3.patch
# 2. Use pnpm patch to create a pnpm-compatible patch:
pnpm patch some-package@1.2.3
# Edit the temp folder — copy your existing changes there
pnpm patch-commit /tmp/.pnpm-patch-xxx/some-package
# Creates patches/some-package@1.2.3.patch (pnpm format)
# Updates package.json patchedDependencies
# 3. Remove patch-package:
npm uninstall patch-package
# Remove postinstall: "patch-package" from package.json
# Remove old patches/ files (replaced by pnpm's patches/)
# 4. Verify:
pnpm install
# Should say: "Applying patches..." for each patch
From patch-package to yarn patch:
# 1. For each existing patch:
yarn patch some-package
# Edit the temp folder with your existing changes
yarn patch-commit -s /tmp/xfs-xxx/some-package
# Creates .yarn/patches/some-package-npm-1.2.3-xxxx.patch
# Updates package.json resolutions
# 2. Remove patch-package:
yarn remove patch-package
# Remove postinstall hook from package.json
# 3. Delete old patches/ directory:
rm -rf patches/
Patch Maintenance Best Practices
Patches accumulate over time and can become a maintenance burden if not managed proactively. These practices apply regardless of which patching tool you use.
Document every patch. Add a comment at the top of each patch file explaining why it exists, what upstream issue it references, and under what conditions it can be removed:
# patches/some-package+2.3.1.patch
# WHY: processList() returns null on Windows paths with spaces (upstream bug #456)
# UPSTREAM: Fixed in v2.3.2 (not yet released as of 2026-02-01)
# REMOVE WHEN: upgrade to >= 2.3.2
# TESTED: Verified fix on Windows, macOS, Linux CI
Audit patches after every dependency upgrade. Set up a CI step that verifies patches still apply cleanly after npm install:
# In CI after install, verify patches apply:
npx patch-package --error-on-fail # Exits non-zero if any patch fails
# or for pnpm:
pnpm install # Will error if a patchedDependency's patch doesn't apply
Remove patches when no longer needed. After upgrading a patched dependency to a version where the issue is fixed, delete the patch file and remove the entry from package.json. Leaving stale patches creates noise and can conflict with future upgrades.
Prefer upstream contributions over long-lived patches. A patch that lives longer than 3 months usually means the upstream maintainer is unresponsive or the fix is controversial. At that point, consider forking the package (with a different npm scope) or replacing it with an alternative. Patches are a short-term fix, not a long-term strategy.
Test patches in CI on multiple OS environments. Patches often fix Windows-specific issues that developers do not catch locally. Add a matrix CI job that tests on Ubuntu, macOS, and Windows (or at least ubuntu + windows) whenever you add a patch that touches path handling or filesystem operations.
Monorepo Hoisting and Patch Application Edge Cases
In monorepos where dependencies are hoisted to a shared root node_modules, patch-package must be configured at the root level to reach the hoisted dependencies. Running npx patch-package react from a workspace package directory will create the patch in that package's directory, but if React is hoisted to the root, the patch needs to be applied from the root instead. The --patch-dir flag lets you specify where patches are stored and read, allowing a single root-level patches/ directory to serve all workspace packages.
Pnpm's workspace architecture handles patches more cleanly: the patchedDependencies configuration in the root package.json applies patches to the shared virtual store. When pnpm installs packages in a workspace, it reads the root patchedDependencies and applies patches to the virtual store entries before linking them into workspace packages. This means a patch applied once at the workspace root is automatically effective for all workspace packages that depend on the patched version — no per-package patch configuration needed.
Handling TypeScript Declaration File Patches
A frequently overlooked use case for dependency patching is fixing incorrect TypeScript declaration files without modifying the runtime JavaScript. When a package ships wrong types — a callback that the docs say is optional but the types mark as required, or a return type of void when the function actually returns a value — the TypeScript compiler errors are real even if the runtime behavior is correct. Patching the .d.ts file fixes the TypeScript error without affecting the published JavaScript.
All three tools support patching .d.ts files with the same workflow as patching .js files. For pnpm patch, you edit the declaration file in the temporary directory and commit with pnpm patch-commit. The resulting patch file is a standard unified diff targeting the .d.ts file path. One important nuance: declaration files often reference other types via import type statements that resolve relative to the package root. Edits that change these references require corresponding changes to the referenced files, and the patch must include all modified files — a single-file patch that adds a new type import but doesn't add the corresponding type definition will compile on the machine where you created the patch but fail on fresh installs.
Scoped package declaration patches have a naming quirk worth knowing: the @ prefix in scoped package names is problematic in file paths on Windows. patch-package escapes the @ as a + in the patch filename — @types/express+4.17.21.patch — because @ is not valid in file paths on some operating systems. pnpm and yarn handle this differently, using @types+express@4.17.21.patch and URL-encoding the @ respectively. When migrating patches from one tool to another, these naming differences require renaming patch files and updating the configuration entries in package.json — a minor but easy-to-overlook step that causes "patch not found" errors if skipped.
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on patch-package v8.x, pnpm v9.x, and yarn v4.x.
Compare package management and developer tooling on PkgPulse →
See also: taze vs npm-check-updates vs npm-check and pkg-types vs read-pkg vs read-package-up, archiver vs adm-zip vs JSZip (2026).