npm delete, what it leaves behind

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.

M
Matthew Diakonov
10 min read
4.8from npm + GitHub signals
Shows lsof output of an unlinked but still-running binary
Lists the exact TCC + SwiftPM paths npm does not touch
Verified against whatsapp-mcp-macos v1.1.0 on macOS 15

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.

whatsapp-mcp-macos / package.json

And here is what postinstall produces, which npm install drops into {prefix}/lib/node_modules/whatsapp-mcp-macos/.build/release/whatsapp-mcp:

Sources/WhatsAppMCP/main.swift

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.

npm uninstall -g
lockfile update
bin symlink
node_modules/whatsapp-mcp-macos
Running MCP child
SwiftPM cache
TCC grant
MCP host config
Shell hash

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:

npm uninstall -g whatsapp-mcp-macos (host still running)

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:

the inode is marked (deleted) but still backing an fd

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:

Claude CodeCursorFazmZed MCPVS Code MCP extlaunchdPM2Windsurftmux dev serverContinue.dev

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

MCP Hostos / execvewhatsapp-mcp (missing)fork() + execve(whatsapp-mcp)ENOENT: no such fileretries from PATH cacheENOENT againlogs to ~/Library/Logs/ClaudeUI shows the tool as unavailable

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.

1

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.

2

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.

3

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.

4

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.

5

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.

6

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 complete removal of whatsapp-mcp-macos on macOS

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.

FeatureA typical pure-JS package (e.g. chalk)whatsapp-mcp-macos (native postinstall + host-held)
What npm touchesThe package tarball + bin symlinkSame. Nothing else.
Running child process spawned from the packageNot npm's problemKeeps executing from the unlinked inode until the host exits
Shell command lookupStale in the current shell until `hash -r`Stale in the current shell until `hash -r`
Artifacts from postinstallLeft on disk (native builds, downloaded binaries)Left on disk (~/.swiftpm, ~/Library/Caches/org.swift.swiftpm)
macOS TCC (Accessibility, Automation) grantsUntouched. npm has no TCC.db permission.Untouched. You must revoke by hand in System Settings.
Host configuration referencing the packageUntouched (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.