Node.js / Storage

Delete node_modules recursively without wrecking your projects

Published Sep 6, 2026 · 6 min read · macoptimize

Once you know how many dependency folders you have, the next question is how to remove them in bulk without deleting the one you are actively working in. Finder is not the tool for this. A single node_modules tree can contain 60,000 small files, and asking Finder to calculate sizes across a projects directory will spin for a very long time.

List before you delete

Always produce a list first, and always prune so the command does not descend into nested dependency folders:

find ~/Projects -name node_modules -type d -prune 2>/dev/null

Swap the path for wherever you keep code. Narrowing the search to ~/Projects rather than ~ is faster and reduces the chance of catching a folder buried inside an app bundle.

Add size and age, which is what actually decides it

find ~/Projects -name node_modules -type d -prune -exec du -sh {} + | sort -hr

For age, look at the project folder rather than the dependency folder, since the dependency folder is rewritten on every install:

find ~/Projects -maxdepth 2 -name node_modules -type d -prune -exec sh -c 'echo "$(stat -f \"%Sm\" -t \"%Y-%m-%d\" "$1/..") $1"' _ {} \; | sort

Anything with a modification date older than six months is a candidate. Anything from this week is not.

The bulk delete, with a guard

This removes every dependency folder under a path:

find ~/Projects -name node_modules -type d -prune -exec rm -rf {} +

It works, and it is irreversible. Two guard rails make it much safer:

  • Exclude the project you are in. Add -not -path "$PWD/node_modules" so a stray keystroke cannot delete the dependencies of the repo you are inside right now.
  • Print, then delete. Run the same command with -print instead of -exec rm -rf {} + and read the list. It takes ten seconds and catches a wrong path every time.
find ~/Projects -name node_modules -type d -prune -not -path "$PWD/node_modules" -print
Watch out for uncommitted work. Deleting dependencies is safe, but if a project has local changes it cannot rebuild (a private package, a registry that moved, a Node version you no longer have), you have turned a working checkout into a repair job. Check the remote is pushed before clearing a project you touched this year.

Interactive tools, and where they stop helping

Tools such as npkill walk the file system and let you delete folders from a list with the keyboard. They are genuinely useful and they solve the interactive part well. The limitation is that they only know about dependency folders. They cannot tell you that the same disk also holds 40GB of Xcode output, a 90GB Docker image or 15GB of simulator data, which is usually where the bigger wins are.

One pass, every cache

macoptimize treats dependency folders as one category among many. A single scan returns Developer, Browser and System totals with per project detail underneath, so you are not running a different tool for each kind of junk. On the machine in our screenshots the Developer group alone accounted for 102.21 GB.

Keep the list for next time

Save the sized list to a file so the next cleanup is a diff rather than a discovery exercise:

find ~/Projects -name node_modules -type d -prune -exec du -sh {} + | sort -hr > ~/node-modules-audit.txt

Run it monthly and you will see which projects keep earning their dependencies and which ones only ever appear as a line item.

The one liner, explained line by line

Bulk cleanup goes wrong when people copy a command they do not understand. Here is the whole thing with each part spelled out:

find ~/Projects -name node_modules -type d -prune -exec rm -rf {} +
  • ~/Projects is the boundary. Pointing this at ~ means it also walks Library, caches and every synced folder you own, which is slower and occasionally wrong.
  • -name node_modules matches the folder name exactly, so a file called node_modules.md is ignored.
  • -type d restricts it to directories.
  • -prune is the important one. Once a match is found, find stops descending into it, which prevents wasted work inside deeply nested trees.
  • -exec rm -rf {} + removes each match, batching many paths per rm call.

Run the same command with -print in place of the -exec clause first. Reading the list costs ten seconds and catches a mistyped path before it matters.

Delete by age, which is the real rule

Deleting all of them means reinstalling on the projects you are actively working on this week. Deleting none of them means the disk fills again by spring. Age is the sensible middle:

find ~/Projects -maxdepth 4 -name node_modules -type d -prune -mtime +90 -print

Ninety days of inactivity is a reasonable default. If the project is still valuable, the dependencies come back with one command. If you want to be stricter, filter on the parent folder instead, so an entire archived project goes rather than just its dependencies:

find ~/Archive -maxdepth 2 -type d -mtime +180 -prune -exec du -sh {} +

A shell function worth keeping

Put this in ~/.zshrc and the whole job becomes one word:

nmclean() {
  local root="${1:-$HOME/Projects}"
  find "$root" -name node_modules -type d -prune -exec du -sh {} + | sort -hr | head -25
}
nmkill() {
  local root="${1:-$HOME/Projects}"
  find "$root" -name node_modules -type d -prune -mtime +90 -exec rm -rf {} +
}

nmclean only reports, which is the version you will use most. nmkill respects the age rule, so a project you touched this month is left alone even if you forget which ones those are.

When a project will not reinstall afterwards

It is almost never the deletion. The usual causes, in order of likelihood: the lockfile is missing or was never committed, the project needs a private registry that requires a login, the pinned version of a dependency has been unpublished upstream, or the Node version differs from when the lockfile was written. Check the Node version first with node -v and compare it to the version in the project readme or .nvmrc. Switching to the right major version fixes more of these than anything else.

FAQ

Is rm -rf on node_modules ever dangerous?

Only through a mistyped path or an unquoted variable. An empty variable in rm -rf $DIR/ evaluates to rm -rf /, which is why the commands here use literal paths and find's own -exec instead of shell variables. Always print the match list before deleting it.

Should I use pnpm or yarn workspaces instead?

They change where dependencies live, not whether they exist. pnpm hard links from one global store, which removes most of the duplication between projects, and Yarn Plug and Play removes node_modules entirely. The cleanup still matters because the store itself grows, but there is far less to clean.

What if a project will not reinstall afterwards?

Check the Node version first, then the lockfile, then registry access. A deleted node_modules folder is almost never the cause of a failed install, and the fix is nearly always matching the Node major version or logging in to the registry the project depends on.

How do I exclude a folder from the search?

Add a prune clause before the match, for example -path '*/vendor/*' -prune -o, repeated for each folder to skip. Pruning early also makes the scan faster, since find never walks into the excluded tree.

Does this work on external drives and network shares?

It works on external drives, and it is usually worth running there first because those disks fill up and are often forgotten. Skip network shares: walking a large SMB mount is slow, and deleting over the network is far less forgiving if you get the path wrong.

Or let it do the work

macoptimize scans the folders in this guide, shows the real sizes, and clears what you select. $20 one time, covers 2 Macs.