Visitar URL original
Windows: EPERM on rmdir node_modules/@scope during install with nodeLinker: hoisted (parallel cleanup in pruneStaleWorkspaceHoists, regression in 11.28.0) · Issue #16690 · pnpm/pnpm · GitHub
Skip to content

Windows: EPERM on rmdir node_modules/@scope during install with nodeLinker: hoisted (parallel cleanup in pruneStaleWorkspaceHoists, regression in 11.28.0) #16690

Closed

Description

@Meno96

Last pnpm version that worked

11.27.1

pnpm version

11.28.0 – 11.28.5 (also the current main)

Code to reproduce the issue

On Windows:

# package.json
{ "name": "repro", "private": true, "packageManager": "pnpm@11.28.5" }

# pnpm-workspace.yaml
packages:
  - 'packages/*'
nodeLinker: hoisted

# packages/a/package.json, packages/b/package.json, packages/c/package.json
{ "name": "@scope/a", "version": "1.0.0" }   # @scope/b, @scope/c
pnpm install
pnpm install --force   # repeat

pnpm install --force failed 13 times out of 20 here. In our real monorepo (three workspace projects under one scope, nodeLinker: hoisted), every pnpm install that does not hit the "Already up to date" fast path fails. That includes the install that verifyDepsBeforeRun starts before pnpm run, so git hooks that call pnpm fail too.

Expected behavior

The install succeeds, as on 11.27.1 and on Linux.

Actual behavior

[EPERM] EPERM: operation not permitted, rmdir 'C:\...\repro\node_modules\@scope'
    at async Object.rmdir (node:internal/fs/promises:824:10)
    at async removeEmptyWorkspaceParents (.../pnpm/dist/pnpm.mjs)
    at async Promise.allSettled (index 1)
    at async pruneStaleWorkspaceHoists (.../pnpm/dist/pnpm.mjs)
    at async headlessInstall (.../pnpm/dist/pnpm.mjs)

Additional information

On these installs pruneStaleWorkspaceHoists receives an empty next. I logged it in a local copy of 11.28.5:

previous: { "packages/a": { "@scope/a": "public" }, "packages/b": { "@scope/b": "public" }, "packages/c": { "@scope/c": "public" } }
next: {}

So every workspace link is treated as stale. The links are unlinked, and then the empty-parent cleanup runs in parallel for all of them (Promise.allSettled(staleLinks.map(removeEmptyParentsOfWorkspaceLink))). That calls fs.promises.rmdir on the same node_modules/@scope directory several times at once.

On Windows, a concurrent rmdir of a directory that another call is already deleting fails with EPERM (delete pending), not ENOENT. removeEmptyWorkspaceParents only ignores ENOENT, ENOTEMPTY and EEXIST, so the whole install fails. The restored links leave node_modules in the same state, so the next install fails the same way.

Plain Node.js shows the same race without pnpm:

import fs from 'node:fs/promises'
await fs.mkdir('nm/@scope', { recursive: true })
for (const n of ['a', 'b', 'c']) await fs.symlink(process.cwd(), `nm/@scope/${n}`, 'junction')
await Promise.all(['a', 'b', 'c'].map((n) => fs.unlink(`nm/@scope/${n}`)))
console.log(await Promise.allSettled([1, 2, 3].map(() => fs.rmdir('nm/@scope'))))
// Windows: a mix of fulfilled, ENOENT and EPERM (24 EPERM out of 60 calls over 20 runs)

Possible fixes, either one:

  • Run the parent cleanup sequentially, or dedupe the parent directories before removing them.
  • Treat EPERM like ENOENT here on win32 (and EBUSY), or check that the directory still exists before failing.

A separate question is whether next should be empty on these installs. Removing and relinking every workspace hoist on each --force or headless install looks unintended.

Node.js version

22.22.2

Operating System

Windows

Activity

  1. github-actions commented on Oct 7, 2026

    @github-actions
    Contributor

    @Meno96, are you planning to open a pull request for this? If so, reply here and a maintainer will assign the issue to you, so nobody else starts on the same fix.

  2. Meno96 commented on Oct 14, 2026

    @Meno96
    Author

    Thanks for the quick fix in #16693! Is a v11 release that includes it planned soon? We're on v11 (not ready for v12 yet) and this blocks our migration on Windows with nodeLinker: hoisted, so knowing a rough timeline would help us plan.

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

    area: monorepoEverything related to the pnpm workspace feature

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions