OpusMill

EPERM: operation not permitted on Windows

Verified on Windows 11, Node 24.20.0 · OpusMill

Error: EPERM: operation not permitted, unlink 'C:\...\file'
Error: EPERM: operation not permitted, rmdir 'C:\...\dir'
The short version

The standard advice — “something has the file open” and “the file is read-only” — is mostly wrong on a current Windows machine. Neither of those produces EPERM. The causes that do are narrower, which makes this much easier to diagnose than the folklore suggests.

What I measured

Every line below was run on Windows 11 with Node 24.20.0, not recalled from an answer somewhere:

unlink a file THIS process holds open (read handle)  -> deleted, no error
unlink a file ANOTHER process holds open            -> deleted, no error
unlink a file with the read-only attribute set      -> deleted, no error
rmdir a directory that is a process's cwd           -> EBUSY
unlink an .exe that is currently running            -> EPERM
The usual claimWhat actually happens
“A process has it open” Not EPERM. Node opens files with FILE_SHARE_DELETE, so a handle it holds does not block deletion — not even another Node process's.
“The file is read-only” Not EPERM. Node clears the attribute and deletes it anyway.
“The directory is in use” EBUSY, not EPERM. A different error code, and worth knowing — if you are seeing EBUSY, look for a process whose working directory is inside the folder.

So what does cause it

  1. The file is a running executable or a loaded DLL. Reproduced above. Windows maps a running image and will not let you delete it. If your build deletes dist/ while something from dist/ is still running — a dev server, a spawned worker, an Electron process — this is your bug. It is also why it looks intermittent: it depends on whether the previous run has exited yet.
  2. Antivirus or a search indexer has the file open without FILE_SHARE_DELETE. This is the genuine version of “something has it open”: not any handle, only a handle opened without sharing delete. It is why the error is often intermittent and disappears on retry, and why excluding your node_modules from real-time scanning helps.
  3. You genuinely lack permission — a file owned by another user, or one under a protected directory. Rare in a project folder; common if a step ran elevated once and left root-owned files.
  4. The path is a directory and you called a file operation on it (or the reverse). Windows reports this as EPERM where POSIX would say EISDIR.

How to find the holder

Rather than guessing, ask the system which process has the handle. From an administrator PowerShell:

Get-Process | Where-Object { $_.Path -like "*\dist\*" }

For the general case, Sysinternals handle.exe names the process holding any path:

handle.exe -nobanner "C:\path\to\your\file"

Windows also ships Resource Monitor (resmon), whose CPU tab has an “Associated Handles” search box that does the same job with no download.

What to do about it in a build

  • Stop the process before you delete its output. The fix is almost always ordering, not retrying.
  • Do not paper over it with a retry loop unless the cause is genuinely antivirus. A retry that hides a “we deleted a running server's binary” bug will make it flaky rather than fixed. rimraf retries by default, which is convenient and can also hide this.
  • Exclude your working directory from real-time scanning if you are on a managed machine and the failures are random. That is a real, common cause — unlike the two the folklore blames.

Check your own project for the rest of this class of bug. Paste your code or your package.json into the browser checker — nothing is uploaded, it runs on your machine — or run npx github:Hackierz/winbreak over the whole repository. --fix repairs the npm scripts that have one obvious answer and refuses the ones that need a human.

Other errors in the same family:

Background: I scanned the 600 most-downloaded npm CLI packages — 17.4% have a package.json script that cannot run on Windows.