cheat sheet

rimraf

Package-level reference for rimraf on npm — install, API, why it still exists in the Node fs.rm era, and migration paths.

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

bash
# 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).

bash
# 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.x and 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/rimraf needed (and @types/rimraf is 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.rm covers 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:

ConsumerHow
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

ToolTrade-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.rmSame as above, async. The recommended modern API.
shx rm -rfThe shx package implements POSIX rm (and other commands) cross-platform. Bundles all common commands; bigger surface than rimraf.
del / del-cliSindre Sorhus's alternative with glob-by-default and safety prompts. Smaller adoption.
trashSends 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

  1. rimraf v5+ is async-default; v4 and earlier are callback-default. rimraf("dist") returns a promise in v5+. In v4 you needed rimraf("dist", () => {}) or used rimraf.sync. Lockfiles pinned to v3 still appear in many projects.
  2. preserveRoot is on by default — but only for /. Calling rimraf("/") errors out; calling rimraf("/something") does not — so a typo in a script can still nuke the wrong directory. Validate paths.
  3. 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.
  4. Symbolic links to directories. Default behaviour is to remove the symlink, not the target — usually what you want. --no-preserve-root does NOT change this.
  5. Read-only files on Windows. Pre-v3, rimraf would fail to delete read-only files in node_modules. v3+ added auto-chmod fallback; v4+ uses fs.rm({ force: true }) which handles this transparently.
  6. bin/rimraf and library export are the same package. Importing from rimraf gives you rimraf, rimrafSync, glob helpers, etc. The CLI is built from the same module.
  7. No EPERM retry by default. On Windows, file-locking by AV / IDEs can briefly block deletion. Pass --retryDelay=100 --maxRetries=5 to retry, or use the retry option in code.

Real-world recipes

Delete a build output directory

The single most common use:

json
// package.json
{
  "scripts": {
    "clean": "rimraf dist",
    "prebuild": "npm run clean"
  }
}
bash
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.

bash
# 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)

javascript
// 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:

javascript
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.

javascript
// 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:

javascript
// 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:

bash
npx rimraf node_modules --retry-delay=500 --max-retries=10

Output: (none — exits 0 on success)

javascript
await rimraf("node_modules", { retryDelay: 500, maxRetries: 10 });

Drop-in for cross-platform package.json scripts

json
{
  "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().length workers. No knob.
  • For huge trees (>10⁶ files), fs.rm in Node 20+ is faster because rimraf adds some abstraction overhead. Use fs.rm directly 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 → ToHighlights
2 → 3Glob support. New options object. Mostly back-compat.
3 → 4Breaking. Switched to promises-default. Dropped glob dep — bundles native glob. rimraf("path") returns a promise; old rimraf("path", cb) errors.
4 → 5ESM-default. Drops Node 14. Same API surface.
5 → 6Drops Node 18-. Type packaging refined. Drop-in for v5 users.

Common migration friction

Code written for rimraf@3 that uses the callback form:

javascript
// 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

  1. rimraf("/") and rimraf("$HOME") are footguns. preserveRoot: true blocks / but not unknown roots like C:\ on Windows pre-v4. Always validate paths against allowlists.
  2. 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.
  3. Symlink escape. If your build runs under a writable workspace, an attacker who creates node_modules/x -> /etc can convince rimraf node_modules to delete unrelated paths. v4+ handles symlinks safely (removes the link, not the target) — keep up to date.
  4. 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:

ToolHow rimraf shows up
npm-run-all2, concurrentlyrimraf is the canonical first step in clean chains
tsc -b --clean alternativeWhen tsc --clean doesn't catch everything, rimraf '**/*.tsbuildinfo' '**/dist' does
next clean, nuxt cleanup, astro checkAll internally shell out to rimraf or its bundled version
Lerna / Turbo / Nxclean 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-cli or trash for safer semantics.
  • You need to send to OS trash. Use the trash package — same author as del.
  • You only ever need to delete one literal path. fs.rm is enough; rimraf's value is glob + cross-platform CLI.
  • Your project's minimum Node already excludes the pre-fs.rm era. 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