rimraf#
What it is#
rimraf is the canonical npm package for “delete this directory and everything inside it, recursively, cross-platform” — the JavaScript equivalent of rm -rf. It first appeared in 2011, became the implicit dependency of nearly every clean script in JavaScript history, and remains one of the most-downloaded packages on npm today (hundreds of millions of weekly downloads, almost all transitive).
The original raison d’être was that fs.rmdir recursively didn’t work on Windows, and rm -rf doesn’t exist there. Node 14.14+ added fs.rm({ recursive: true, force: true }), which obviates the most common use case — but rimraf still ships because (a) it works without code changes across Node versions, (b) it supports glob patterns natively, and (c) the rimraf CLI is a familiar package.json script invocation.
Install#
# As a CLI dev dep (most common — used in scripts)
npm install -D rimraf
pnpm add -D rimraf
yarn add -D rimraf
# As a library
npm install rimraf
Output: rimraf binary on PATH under node_modules/.bin/rimraf. Library import available as rimraf (ESM, CJS, and CLI all in one package).
# One-off without installing
npx rimraf dist
Output: (none — silently deletes ./dist and exits 0)
Versioning & Node support#
- Current major line is
6.x(released 2024). Drops Node 18-, adds native ESM, type definitions in-tree. 5.x(2023) was the breaking-change line — switched from callback-only to promise-default API.4.xand earlier are callback-style and CJS-only — still widely used in legacy lockfiles.- Recent releases require Node 20+. v5 still supports Node 14+.
- TypeScript types are built in since v4 — no
@types/rimrafneeded (and@types/rimrafis deprecated/empty for v4+).
Package metadata#
- Maintainer: Isaac Z. Schlueter (npm creator) and the npm core team.
- Project home: github.com/isaacs/rimraf
- npm: npmjs.com/package/rimraf
- License: ISC
- First released: 2011
- Downloads: ~80-100 million per week — overwhelmingly transitive (every test runner, every bundler depends on it). Direct downloads have plateaued as
fs.rmcovers more cases.
Peer dependencies & extras#
rimraf v4+ has zero runtime dependencies — it’s a pure stdlib wrapper. Older versions (<4) depended on glob. The migration from glob@7 → bundled glob support is the main reason rimraf v4 ships a much smaller install footprint than the legacy chain.
No common companion packages — rimraf is a leaf utility. Indirectly used by:
| Consumer | How |
|---|---|
| Build tools (webpack, rollup, esbuild) | Clean output directories |
| Test runners (jest, mocha, vitest) | Cache cleanup |
| Monorepo tools (turbo, nx, lerna) | Per-package clean scripts |
| Frameworks (Next.js, Nuxt, Astro) | .next, .nuxt, dist cleanup |
Alternatives#
| Tool | Trade-off |
|---|---|
fs.rm({ recursive: true, force: true }) (Node 14.14+) | Built-in, no dep. Use this for new code with a fixed minimum Node target. No glob support — only literal paths. |
fs.promises.rm | Same as above, async. The recommended modern API. |
shx rm -rf | The shx package implements POSIX rm (and other commands) cross-platform. Bundles all common commands; bigger surface than rimraf. |
del / del-cli | Sindre Sorhus’s alternative with glob-by-default and safety prompts. Smaller adoption. |
trash | Sends to OS trash instead of permanent delete. Use when “rm” is too destructive. |
rm -rf (POSIX) + Remove-Item -Recurse (PowerShell) | Direct shell. Fastest, but cross-platform scripts need two paths. |
Common gotchas#
rimrafv5+ is async-default; v4 and earlier are callback-default.rimraf("dist")returns a promise in v5+. In v4 you neededrimraf("dist", () => {})or usedrimraf.sync. Lockfiles pinned to v3 still appear in many projects.preserveRootis on by default — but only for/. Callingrimraf("/")errors out; callingrimraf("/something")does not — so a typo in a script can still nuke the wrong directory. Validate paths.- Glob expansion is shell-dependent.
rimraf "dist/**"works because rimraf does the globbing;rimraf dist/**may have the shell expand it first, with different semantics. Quote glob args. - Symbolic links to directories. Default behaviour is to remove the symlink, not the target — usually what you want.
--no-preserve-rootdoes NOT change this. - Read-only files on Windows. Pre-v3, rimraf would fail to delete read-only files in
node_modules. v3+ added auto-chmod fallback; v4+ usesfs.rm({ force: true })which handles this transparently. bin/rimrafand library export are the same package. Importing fromrimrafgives yourimraf,rimrafSync,globhelpers, etc. The CLI is built from the same module.- No
EPERMretry by default. On Windows, file-locking by AV / IDEs can briefly block deletion. Pass--retryDelay=100 --maxRetries=5to retry, or use theretryoption in code.
Real-world recipes#
Delete a build output directory#
The single most common use:
// package.json
{
"scripts": {
"clean": "rimraf dist",
"prebuild": "npm run clean"
}
}
npm run clean
Output: (silent; exits 0 whether the dir existed or not — force: true is default)
Glob-based deletion#
rimraf understands ** and other glob patterns natively — quote them so the shell doesn’t expand first.
# Delete every .log file in the tree
npx rimraf "logs/**/*.log"
# Delete every .turbo cache directory anywhere in the monorepo
npx rimraf "**/.turbo"
Output: (none — exits 0 on success)
// Programmatic
import { rimraf } from "rimraf";
await rimraf(["dist/**", "coverage/**", "*.tsbuildinfo"], { glob: true });
Preserve-root for safety#
preserveRoot (default true) blocks deletion of the filesystem root, but you can extend it to other paths you want to protect:
import { rimraf } from "rimraf";
await rimraf("dist", {
preserveRoot: true,
filter: (path) => {
if (path.includes("/critical-data/")) return false;
return true;
},
});
The filter callback returns false to skip a file/directory. Use it for “delete everything except…” patterns.
Sync vs async#
Sync blocks the event loop; for large trees on a busy server, prefer async.
// Async (recommended)
import { rimraf } from "rimraf";
await rimraf("./big-cache");
// Sync (legacy)
import { rimrafSync } from "rimraf";
rimrafSync("./big-cache");
For one-off cleanup in postinstall or build scripts, sync is fine — they’re already serial.
Migrating to fs.rm#
If your minimum Node is 14.14+, you can drop rimraf entirely:
// Before
import { rimraf } from "rimraf";
await rimraf("./dist");
// After
import { rm } from "node:fs/promises";
await rm("./dist", { recursive: true, force: true });
The catch: no glob support. For glob deletion, either keep rimraf or pair fs.rm with globby / fast-glob.
CI-safe cleanup with retries#
On Windows CI agents, antivirus or git can transiently lock files. Retry to ride out the lock:
npx rimraf node_modules --retry-delay=500 --max-retries=10
Output: (none — exits 0 on success)
await rimraf("node_modules", { retryDelay: 500, maxRetries: 10 });
Drop-in for cross-platform package.json scripts#
{
"scripts": {
"clean": "rimraf dist coverage .nyc_output *.tsbuildinfo",
"clean:deps": "rimraf node_modules",
"fresh": "npm run clean:deps && npm install && npm run build"
}
}
The exact same string works on macOS, Linux, and Windows — which is the entire reason rimraf is still ubiquitous despite fs.rm existing.
Production deployment#
rimraf is a dev/build-time tool. It should NOT ship in production runtime code paths:
- Build scripts: fine.
- Test fixtures: fine.
- Runtime cleanup in a server: prefer
fs.rm— one less dep in the prod tree.
If you must keep it as a runtime dep (legacy code), check package.json dependencies vs devDependencies — most projects accidentally have it in dependencies due to bad copy-paste.
Performance tuning#
- Parallelism is automatic in v4+. It walks the tree concurrently up to
os.cpus().lengthworkers. No knob. - For huge trees (>10⁶ files),
fs.rmin Node 20+ is faster because rimraf adds some abstraction overhead. Usefs.rmdirectly for one-shot deletes. - Filter callbacks slow things down. Each path traversal calls the filter. If you don’t need filtering, omit it — saves ~10-15% on large trees.
Version migration guide#
| From → To | Highlights |
|---|---|
| 2 → 3 | Glob support. New options object. Mostly back-compat. |
| 3 → 4 | Breaking. Switched to promises-default. Dropped glob dep — bundles native glob. rimraf("path") returns a promise; old rimraf("path", cb) errors. |
| 4 → 5 | ESM-default. Drops Node 14. Same API surface. |
| 5 → 6 | Drops Node 18-. Type packaging refined. Drop-in for v5 users. |
Common migration friction#
Code written for rimraf@3 that uses the callback form:
// Old (rimraf@3)
rimraf("./dist", (err) => { if (err) throw err; });
// New (rimraf@4+)
import { rimraf } from "rimraf";
await rimraf("./dist");
Old default import (require("rimraf")) returned the function directly; new packages use a named export.
Security considerations#
rimraf("/")andrimraf("$HOME")are footguns.preserveRoot: trueblocks/but not unknown roots likeC:\on Windows pre-v4. Always validate paths against allowlists.- Untrusted input as a glob. Never pass user input as a rimraf path or glob. A leading
..segment can traverse out of the intended directory. - Symlink escape. If your build runs under a writable workspace, an attacker who creates
node_modules/x -> /etccan convincerimraf node_modulesto delete unrelated paths. v4+ handles symlinks safely (removes the link, not the target) — keep up to date. - CI runners are often super-user. A rogue
rimraf $(some-script)in a Dockerfile can wipe the build environment. Pin script outputs; lint for empty //expansions.
Ecosystem integrations#
rimraf is a utility — it doesn’t have an ecosystem of its own. But it underpins:
| Tool | How rimraf shows up |
|---|---|
npm-run-all2, concurrently | rimraf is the canonical first step in clean chains |
tsc -b --clean alternative | When tsc --clean doesn’t catch everything, rimraf '**/*.tsbuildinfo' '**/dist' does |
next clean, nuxt cleanup, astro check | All internally shell out to rimraf or its bundled version |
| Lerna / Turbo / Nx | clean task in pipelines uses rimraf cross-platform |
When NOT to use this#
- You can require Node 20+. Use
fs.rm({ recursive: true, force: true })— built-in, no dep. - You want a confirmation prompt. rimraf has none — use
del-cliortrashfor safer semantics. - You need to send to OS trash. Use the
trashpackage — same author asdel. - You only ever need to delete one literal path.
fs.rmis enough; rimraf’s value is glob + cross-platform CLI. - Your project’s minimum Node already excludes the pre-
fs.rmera. Modernize and drop the dep — the install size, while small, adds up across a large dep graph.
That said: in package.json scripts, rimraf is still the most readable cross-platform option, and the install cost is negligible. Many projects keep it as a devDependency purely for the script string portability.
See also#
- JavaScript: node-fs —
fs.rm,fs.promises, filesystem primitives - Concept: filesystem — paths, links, recursion semantics
- Packages: npm-concurrently — companion in
clean && buildchains