EPERM: operation not permitted on Windows
Verified on Windows 11, Node 24.20.0 · OpusMill
Error: EPERM: operation not permitted, rmdir 'C:\...\dir'
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 claim | What 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
-
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 fromdist/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. -
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 yournode_modulesfrom real-time scanning helps. - 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.
-
The path is a directory and you called a file operation on
it (or the reverse). Windows reports this as
EPERMwhere POSIX would sayEISDIR.
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.
rimrafretries 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:
- The fix for 'NODE_ENV' is not recognized
- The fix for 'rm' is not recognized
- The fix for spawn EINVAL on Windows
- The fix for ENOENT on node_modules/.bin
- Path too long on Windows
Background: I scanned the 600 most-downloaded
npm CLI packages — 17.4% have a package.json
script that cannot run on Windows.