The Problem node_modules Is a Disk Space Leak
A fresh npm install on a typical Node.js project pulls in anywhere from 50MB to 500MB of dependencies. That's fine for one project. But most developers have dozens of projects cloned on their machine side projects, client work, open source contributions, tutorials they started and forgot about.
Those node_modules folders sit there indefinitely. You don't need them unless you're actively working on that project. But they're invisible you don't see them in Finder or Explorer unless you go looking. Meanwhile they quietly consume gigabytes.
The manual approach is tedious: open each project folder, check if there's a node_modules, estimate the size, decide whether to delete it. Repeat for 40 projects. Nobody does this consistently.
So I built deepclean-cli a single command that scans an entire directory tree, finds every matching folder, tells you exactly how much space each one is using, and removes them after a confirmation prompt.
Install and Run
Install it globally once and use the deepclean command anywhere:
npm install -g deepclean-cliOr run it without installing via npx:
npx deepclean-cli -d ~/codePoint it at your projects folder and run a dry-run first to see what it finds:
deepclean -d ~/code --dry-runOutput looks like this:
Scanning "/Users/zain/code" up to 3 level(s) deep for "node_modules"... (dry run)
Found: /Users/zain/code/project-alpha/node_modules (312.45 MB)
Found: /Users/zain/code/project-beta/node_modules (87.20 MB)
Found: /Users/zain/code/client/saas-app/node_modules (540.11 MB)
Found: /Users/zain/code/experiments/ai-chat/node_modules (203.78 MB)
4 folder(s) found, totaling 1.14 GB.
Dry run complete. Nothing was deleted.When you're ready to delete, run it without --dry-run. It will show you the same list and ask for confirmation before touching anything:
deepclean -d ~/code
# Output:
# Found: ... (lists all matches with sizes)
# 4 folder(s) found, totaling 1.14 GB.
# Delete 4 folder(s)? [y/N]All the Options
The tool is intentionally simple five flags cover every use case:
deepclean --help
Options:
-d, --dir <path> Root directory to scan (required)
-t, --target-dir <name> Folder name to find/delete (default: "node_modules")
-l, --level <number> Max depth to scan (default: 3)
--dry-run List matches, delete nothing
-y, --yes Skip the confirmation prompt
-V, --version Print version
-h, --help Print usageClean dist and build folders too it works on any folder name, not just node_modules:
# Clean up dist output folders across all projects
deepclean -d ~/code -t dist
# Clean up .cache folders, scan deeper
deepclean -d ~/code -t .cache -l 5Skip the confirmation in scripts the -y flag is useful in automation:
# In a cron job or shell script no interactive prompt
deepclean -d ~/code -yHow It Works Internally
The core logic is a recursive directory scanner in about 80 lines of plain Node.js no dependencies beyond commander for CLI argument parsing.
The scanner walks the directory tree depth-first, up to the configured level. When it finds a folder matching the target name, it calculates the size using a recursive byte counter, records the match, and critically never descends into it. This is important: if you're scanning for node_modules, you don't want to waste time recursing into a folder you're about to delete anyway.
// The key design decision stop descending once a match is found
if (entry.name === targetName) {
const size = getDirSize(fullPath);
stats.count++;
stats.bytes += size;
if (options.onMatch) options.onMatch(fullPath, size);
if (!options.dryRun) {
fs.rmSync(fullPath, { recursive: true, force: true });
}
continue; // don't recurse into the matched folder
}
// Only recurse into non-matching directories
scanDir(fullPath, targetName, maxLevel, depth + 1, options, stats);The CLI always runs a dry-run pass first even when you haven't specified --dry-run. This means you always see the full list and total size before anything gets deleted. Then it asks for confirmation (unless you passed -y), and only then runs the actual deletion pass.
// Two-pass approach in the CLI
// Pass 1: always dry-run show matches and total size
const preview = run({ dir, targetDir, level, dryRun: true, onMatch, onError });
if (preview.count === 0) { console.log("No matches found."); return; }
if (opts.dryRun) { console.log("Dry run complete."); return; }
// Pass 2: confirm then delete
const ok = opts.yes || await confirm(`Delete ${preview.count} folder(s)? [y/N] `);
if (!ok) { console.log("Aborted."); return; }
run({ dir, targetDir, level, dryRun: false, onMatch, onError });Deletion uses fs.rmSync with recursive: true folders are permanently removed, not sent to the trash. Always run --dry-runfirst if you're unsure what will be matched.
Why I Kept It Simple
There are fancier tools out there that do similar things interactive TUIs, progress bars, undo stacks. I deliberately avoided all of that.
- ▸Zero config one required flag (-d), sensible defaults for everything else
- ▸Single dependency (commander) installs in under a second, no bloat
- ▸Works on Node.js 14+ runs on anything you're likely to have
- ▸Composable the scanner is exported as a module so you can use it programmatically in your own scripts
- ▸Predictable always shows you what it found before deleting, no surprises
The goal was a tool I'd actually remember to use one command, one obvious flag, done.
Use It Programmatically
The scanner is also available as a Node.js module if you want to integrate it into your own tooling:
const { run, formatBytes } = require("deepclean-cli/lib/scanner");
const result = run({
dir: "/Users/zain/code",
targetDir: "node_modules",
level: 3,
dryRun: true,
onMatch: (matchPath, size) => {
console.log(`Found: ${matchPath} (${formatBytes(size)})`);
},
});
console.log(`Total: ${formatBytes(result.bytes)} across ${result.count} folders`);
// result.matches is an array of { path, size } objectsGet It
- ▸npm: npm install -g deepclean-cli
- ▸npx (no install): npx deepclean-cli -d ~/code --dry-run
- ▸GitHub: github.com/ZainMustafaaa PRs and issues welcome
If you've been putting off cleaning up your projects folder because hunting through directories manually is tedious this is the 30-second fix. Run the dry-run first, see what's sitting there, and reclaim the space.