npm delete package, and the five things the command leaves behind
Every guide on this question stops at npm uninstall <pkg>. That is half the answer. The moment the package you are deleting compiled native code in a postinstall script, spawned a process a long-running host is still holding, or asked for a macOS privacy permission, the “delete” leaves a trail.
This guide uses whatsapp-mcp-macos as the worked example: a native MCP server that runs xcrun swift build -c release in postinstall and asks for Accessibility on first use. Everything below is observable on your own machine with lsof, hash, and System Settings.
The mental model most guides assume
A pure-JavaScript npm package is inert. It is a directory of files plus a little metadata. Removing it is almost atomic: delete the folder, update package.json, update package-lock.json. Nothing else in the system cares.
Generic advice follows that model. Every article you will find on this topic lists npm uninstall, npm remove, and npm rm as aliases, mentions the -g flag for global installs, suggests npm prune if you edited package.json by hand, and stops. Accurate, but incomplete the moment a package does anything in postinstall.
The shape of the package that breaks the model
Here is the package.json that changes things. Three fields matter: os, scripts.postinstall, and bin.
And here is what postinstall produces, which npm install drops into {prefix}/lib/node_modules/whatsapp-mcp-macos/.build/release/whatsapp-mcp:
What npm touches vs. what npm leaves behind
One npm command writes to three things on disk. Five other things stay where they were. This diagram is the one-picture version of the rest of the article.
npm uninstall goes to disk. Five other surfaces are untouched.
The five leftovers, named
Each card below is a thing you can observe on your machine after running npm uninstall -g whatsapp-mcp-macos. None of them are npm bugs. They are all boundaries where npm is not the source of truth.
1. The running child process
POSIX keeps an unlinked executable's inode alive until the last file descriptor closes. A Claude Code session that spawned whatsapp-mcp keeps answering MCP calls after npm uninstall. lsof prints the path with a trailing (deleted).
2. The shell command hash
bash and zsh remember `which whatsapp-mcp` the first time you call it. After npm uninstall, `whatsapp-mcp` may still resolve in your shell until you run `hash -r` (bash) or `rehash` (zsh). You can spawn a brand new orphan that way.
3. The SwiftPM build cache
postinstall ran `xcrun swift build -c release`, which populates ~/.swiftpm and ~/Library/Caches/org.swift.swiftpm with the MCP Swift SDK and MacosUseSDK. npm uninstall has no idea those exist. Next install reuses them.
4. The TCC Accessibility grant
macOS stored Accessibility consent keyed on the binary path (…/.build/release/whatsapp-mcp). The row survives in /Library/Application Support/com.apple.TCC/TCC.db after the binary disappears. A reinstall at the same path re-uses the stale grant silently.
5. The MCP client config
Your MCP host (Claude Code, Cursor, Fazm) still has an entry in ~/.claude.json or mcp.json that points at `whatsapp-mcp`. Next launch, the host tries to spawn it, fails, and logs an error you only see if you check logs.
The unlinked-but-still-running case
Here is the trace. I uninstalled whatsapp-mcp-macos globally while a Claude Code session was open. npm reports success. A second later I asked the same MCP server for its status:
The process is still alive because Claude Code forked and execved the binary, loading its pages into memory. The kernel pins the inode until the last descriptor closes. You can see this with lsof:
This is not a macOS quirk; it is POSIX-standard behaviour. The same shape exists on Linux whenever a long-lived parent spawned the CLI as a child. It is also why you cannot check “is the package really gone” with npm list alone; npm queries disk state, not process state.
anchor fact
0 surfaces npm never touches
The npm client speaks to exactly one directory: node_modules (plus its lockfile). It does not speak to the kernel, the shell, SwiftPM, TCC, or your MCP host config. A full deletion of a native macOS npm package like whatsapp-mcp-macos requires touching all five of those other surfaces by hand, in order.
Who else has this shape
Anything with a long-lived host spawning the CLI as a child, or anything that compiles / downloads native code in postinstall, will exhibit the same residue. The “host-held” pattern is especially common in MCP land:
If any of these spawned the package, step 0 of deletion is quitting the host, not running npm uninstall.
The delete commands, without the mystery
Before the full cleanup, a quick reference of the npm commands that actually remove something. If you already know these, skip ahead.
npm uninstall <pkg>
The modern spelling. Removes a package from package.json and node_modules. `npm remove` and `npm rm` are aliases that behave identically. Use `-g` for a global install.
npm uninstall -g <pkg>
Global removal. Deletes the contents of {prefix}/lib/node_modules/<pkg> and its bin symlinks in {prefix}/bin. Run `npm config get prefix` to see where that is on your machine.
npm prune
Removes packages present in node_modules but absent from package.json. Useful after you delete a dependency by hand editing package.json. Safe to run anytime; idempotent.
npm uninstall --save-dev
Alias: `-D`. Removes from devDependencies specifically. With npm 7+ this is inferred by default; the flag is only required if the same package appears in more than one dependency bucket.
rm -rf node_modules && npm install
The nuclear option when resolution goes sideways. Safe in a fresh clone; risky if you have patches, postinstall side effects, or an out-of-sync lockfile. Always check `git status` first.
How the host discovers the package is gone
There is one moment after the uninstall when the host actually notices: the next time it tries to spawn the MCP child. The sequence looks like this:
mcp server spawn after the package was uninstalled
Until you quit and relaunch the host, or delete the mcpServers.whatsapp-mcp row from your config, the host will keep retrying this on every session start.
The full cleanup, six steps, in order
This is the sequence that actually removes the package from your machine. Steps 3 through 6 are the ones generic tutorials omit. Run them in order. Skipping step 1 is what leaves the unlinked inode alive.
Quit the host that spawned it
Before anything else, quit Claude Code / Cursor / Fazm / any process manager that spawned `whatsapp-mcp` as a child. This closes the file descriptor pointing at the compiled binary. Skip this step and the running child keeps serving MCP calls from an inode that is already scheduled for cleanup.
Run the actual uninstall
`npm uninstall -g whatsapp-mcp-macos` (global) or `npm uninstall whatsapp-mcp-macos` (project). This removes the tarball under {prefix}/lib/node_modules and the bin symlink under {prefix}/bin. Verify with `npm list -g whatsapp-mcp-macos`, which now reports ELSPROBLEMS.
Clear the shell command hash
Bash and zsh cache the resolved path of a command the first time you run it. After the uninstall, `type whatsapp-mcp` may still print the old path. Run `hash -r` in bash or `rehash` in zsh. New shells do not need this.
Purge the SwiftPM build cache
postinstall used `xcrun swift build -c release`, which populated ~/.swiftpm and ~/Library/Caches/org.swift.swiftpm with the MCP Swift SDK and MacosUseSDK checkouts. They are safe to delete only if no other local Swift project depends on the cached versions. `rm -rf ~/.swiftpm ~/Library/Caches/org.swift.swiftpm` if you are sure.
Revoke the TCC Accessibility grant
macOS keyed Accessibility trust on the binary path `.../.build/release/whatsapp-mcp`. The grant row survives the file. Open System Settings > Privacy & Security > Accessibility and remove the host app (Claude Code, Fazm, Terminal) entry that was added when you first gave consent. Otherwise a future reinstall at the same path inherits a stale grant you never re-approved.
Remove the server entry from your MCP config
Edit ~/.claude.json (Claude Code), ~/.cursor/mcp.json (Cursor), or your Fazm settings and delete the `whatsapp-mcp` block under `mcpServers`. Otherwise the host tries to spawn a missing binary on next launch, fails, and writes an error to its log that most users never read.
The full cleanup as a copyable transcript
If you like a one-pane version, this is every command above in the order you would run them. Adjust paths for your host (Cursor uses ~/.cursor/mcp.json, Fazm uses a different config).
A pure-JS package vs. a native-postinstall package
The right column is why generic delete guides are wrong for this class of package. Nothing here is specific to whatsapp-mcp-macos; any Swift or native postinstall package on macOS lives in the right column.
| Feature | A typical pure-JS package (e.g. chalk) | whatsapp-mcp-macos (native postinstall + host-held) |
|---|---|---|
| What npm touches | The package tarball + bin symlink | Same. Nothing else. |
| Running child process spawned from the package | Not npm's problem | Keeps executing from the unlinked inode until the host exits |
| Shell command lookup | Stale in the current shell until `hash -r` | Stale in the current shell until `hash -r` |
| Artifacts from postinstall | Left on disk (native builds, downloaded binaries) | Left on disk (~/.swiftpm, ~/Library/Caches/org.swift.swiftpm) |
| macOS TCC (Accessibility, Automation) grants | Untouched. npm has no TCC.db permission. | Untouched. You must revoke by hand in System Settings. |
| Host configuration referencing the package | Untouched (Cursor, VS Code, Claude Code MCP config, launchd) | Untouched (whatsapp-mcp row stays in ~/.claude.json) |
Stuck on a native package that will not cleanly uninstall?
I maintain whatsapp-mcp-macos. Book 15 minutes and we will walk through your leftovers together.
Frequently asked questions
What is the difference between `npm uninstall`, `npm remove`, and `npm rm`?
They are the same command. `npm remove`, `npm rm`, `npm r`, and `npm unlink` all alias to `npm uninstall`. All four edit package.json (removing the dependency), delete the package directory under node_modules, and clean up the bin symlinks. Any one of them works; most guides standardize on `npm uninstall` because it is the least ambiguous.
Why does `whatsapp-mcp` still run after I uninstalled the package?
Because the host process (Claude Code, Cursor, Fazm) spawned it as a child before you ran the delete. On POSIX systems, removing a file that a running process has open does not terminate the process. The kernel keeps the inode alive until the last file descriptor closes. `lsof -p <pid> | grep whatsapp-mcp` will show the path with a trailing `(deleted)`. Quit the host, then re-run the uninstall.
Does `npm uninstall` delete the compiled Swift binary?
Yes, because the binary lives inside the package directory at `.build/release/whatsapp-mcp`. `npm uninstall` removes the whole directory, binary included. What it does NOT clean up is the SwiftPM build cache that holds intermediate products and the MCP Swift SDK checkout. Those live in ~/.swiftpm and ~/Library/Caches/org.swift.swiftpm and are reused by any future `npm install whatsapp-mcp-macos`.
Does `npm uninstall` revoke the macOS Accessibility permission I granted?
No. Accessibility consent is stored in `/Library/Application Support/com.apple.TCC/TCC.db`, a SQLite database npm does not know exists. The row is keyed on the bundle ID of the host app (the one that actually calls the accessibility APIs), not on the npm package. Removing the npm package leaves TCC untouched. Revoke consent manually in System Settings > Privacy & Security > Accessibility if you want a clean slate.
What does `ELSPROBLEMS missing: whatsapp-mcp-macos` mean after I uninstalled it?
It means your root project's package.json still lists the dependency even though node_modules no longer contains it. Common cause: you ran `rm -rf node_modules` but did not edit package.json, or you ran `npm uninstall` inside a different working directory. Fix: run `npm uninstall whatsapp-mcp-macos` from the project root, or delete the line from package.json manually and run `npm install` to refresh the lockfile.
Should I delete `package-lock.json` when I remove a package?
No. `npm uninstall` updates package-lock.json in the same transaction. Deleting the lockfile afterward forces a full re-resolution of every remaining dependency, which can pull in new minor versions you did not ask for and mask the actual removal in git history. Let `npm uninstall` edit the lockfile for you.
How do I remove a package that is still referenced by my Claude Code MCP config?
`npm uninstall -g whatsapp-mcp-macos` removes the binary. That does not edit ~/.claude.json. Open it, find the `mcpServers` block, and delete the `whatsapp-mcp` entry. Restart Claude Code. If you skip this, the host tries to spawn a binary that no longer exists, fails, and logs the error to `~/Library/Logs/Claude` where most people never look.
Can I reinstall immediately after uninstalling, or do I need to wait?
You can reinstall immediately with `npm install -g whatsapp-mcp-macos`. postinstall will recompile the Swift binary, which is fast the second time because the SwiftPM cache in ~/.swiftpm is still populated. One gotcha: if you left a stale TCC Accessibility grant in place, the reinstall inherits it silently. Users who wanted to test a first-run Accessibility prompt should revoke consent before reinstalling.
Does `npm uninstall` kill background processes it launched?
No. npm has no process manager. If postinstall or any lifecycle script launched a daemon, agent, or MCP server, uninstall will not notice. For whatsapp-mcp-macos the postinstall is a pure build step (`xcrun swift build`), so nothing is left running. If your host spawned the CLI, that is the host's child process, not npm's; see question 2.
What is the safe order for deleting a native npm package on macOS?
Quit any long-lived host that spawned the CLI. Run `npm uninstall -g <pkg>`. Run `hash -r` in the shells where you used the CLI. If the package used a compiler cache (SwiftPM, cargo, ccache), decide whether to scrub it. If the package triggered a TCC grant (Accessibility, Automation, Screen Recording), revoke the grant in System Settings. Finally, delete any host config rows that referenced the package. That order avoids the unlinked-inode race and the stale-grant footgun.