npm, held open by a host

npm how to update package, when a host process is keeping it open

The top ten guides all treat an npm package as an inert file on disk. Run npm update, the new file appears, you are done. That model breaks the moment the package is a CLI that a long-running parent has already spawned as a child process. MCP servers, language servers, launchd units, PM2 apps, Nodemon children, VS Code extension hosts: all of them behave the same way, and all of them are invisible to npm list.

This guide uses whatsapp-mcp-macos as the worked example. It is an MCP server, so Claude Code (or any MCP host) spawns it as a stdio child and holds the compiled Swift binary open for the life of the session. You will see exactly where npm update stops, and what you actually have to do to get the new code into the running process.

M
Matthew Diakonov
9 min read
4.8from npm + GitHub signals
Covers the stdio-child-process case that generic guides skip
Real commands verified against whatsapp-mcp-macos v1.0.1
Diagrams the MCP initialize handshake so you can verify the update landed

The shape of the problem

Most npm packages are libraries: a require() at the top of a file, resolved at call time. Replace the file on disk, and the very next invocation sees the new code. That is the mental model every generic npm how to update package guide assumes.

A CLI that a host has spawned as a child process is a different thing. The host ran execve("whatsapp-mcp", ...) once, the kernel loaded the binary pages into memory, and the host now holds an open file descriptor on the executable. npm update writes a new binary at the same path. The kernel does not retroactively edit the process. The running child keeps executing from the original inode until the host closes the fd, which only happens when the child exits.

whatsapp-mcp-macos is exactly this shape. It is an MCP server, registered in your client config. When Claude Code launches, it runs whatsapp-mcp as a stdio child and keeps it alive for the whole session. An update mid-session replaces the file at .../node_modules/whatsapp-mcp-macos/.build/release/whatsapp-mcp but leaves the running binary exactly where it was.

npm update goes to disk. The host still points at the old child.

npm registry
`npm update -g`
SwiftPM postinstall
compiled binary on disk
Running MCP child
Claude Code host
whatsapp_status

Everything on the left finishes successfully. Everything on the right keeps running the previous version until the host process is restarted and the MCP child is respawned.

Three different things called “version”

Before the recipe, get the naming straight. whatsapp-mcp-macos surfaces three different version numbers, and none of them update in lockstep. Guides that say “run npm list to confirm your update” are checking only the first of these.

1. The npm tag

Lives in package.json ("version": "1.1.0"). This is what the registry indexes and what `npm list -g` reports. It moves every publish. Reading it tells you what is on disk, not what is running.

2. The compiled binary mtime

`stat -f '%Sm' $(which whatsapp-mcp)` reads the filesystem timestamp of the release binary that postinstall produced. A successful update bumps this. A failed postinstall leaves the old binary in place even though the npm tag moved.

3. The MCP serverInfo.version

Hardcoded in Sources/WhatsAppMCP/main.swift as version: "3.0.0", returned in the MCP initialize response. This is the only version the host sees. Only a new process spawn can change it; `npm update` cannot.

Where each number actually lives

The npm tag sits in package.json:

whatsapp-mcp-macos / package.json

The server version string is hardcoded in the Swift source and returned in the MCP initialize response:

Sources/WhatsAppMCP/main.swift

What “the host holds it open” looks like in the wild

The case below is not specific to MCP. Anything with a long-lived parent that spawns the npm-installed CLI as a child behaves identically. These hosts all hold CLI binaries open for the life of their process:

Claude CodeCursorFazmVS Code MCP extensionlaunchdPM2systemdnodemontmux dev serverZed

If any of these spawned your package, npm update is step 1 of 2. The host still needs to be told to respawn.

The “npm update succeeded” illusion

Here is the exact terminal trace of an update that appears to succeed. Read the last three lines carefully:

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

npm update finished cleanly. npm list confirms the new tag is on disk. The MCP host is perfectly happy. And the tool call still returns the old serverInfo.version because the PID that is answering was born before the update.

anchor fact

0 version strings, not one

package.json currently publishes "version": "1.0.1". The MCP server hardcodes version: "3.0.0" in Sources/WhatsAppMCP/main.swift. The first updates every npm publish. The second only updates when a release edits main.swift. If you try to infer one from the other, you will misdiagnose whether your install drifted.

How the host actually picks up the new binary

The MCP handshake is where the new version enters the host’s world. The host sends an initialize request, the freshly-spawned server responds with its serverInfo, and only then does the host know what version it is talking to.

mcp initialize handshake after a respawn

MCP Hostwhatsapp-mcp (child)macOS kernelfork() + execve()loads new binary into memorystdio pipes open{ "method": "initialize" }{ "serverInfo": { "version": "3.0.x" } }tools/list11 tools (defined 11 tools)

Note the direction: the version travels from the freshly spawned child back up to the host. It never travels sideways from package.json.

The upgrade recipe, end to end

This sequence is deliberately five steps. Every tutorial you will find on the first page of search results stops at step 3. Steps 4 and 5 are where the new code actually reaches the process doing the work.

1

Check what is upgradable on disk

Run `npm outdated -g whatsapp-mcp-macos`. This talks only to the registry and to your global node_modules. It never touches the running process, so it is always safe to run.

2

Tell the host to stand down first

Quit the MCP host (Claude Code, Cursor, Fazm, or whatever process manager spawned `whatsapp-mcp`). This releases the file descriptor pointing at the compiled binary. On macOS, `lsof -p <host_pid> | grep whatsapp-mcp` will show the fd if it is still open.

3

Run the update

Run `npm install -g whatsapp-mcp-macos@latest` (cross major versions) or `npm update -g whatsapp-mcp-macos` (stay within semver range). The postinstall recompiles the Swift binary. Wait for it to finish; do not ctrl-c it.

4

Relaunch the host

Restart Claude Code / Cursor / your launchd job. The host re-reads its MCP config, spawns a fresh `whatsapp-mcp` child process, and the new binary loads into memory. A new PID and a new `startedAt` timestamp confirm the respawn.

5

Verify across the wire

Call any tool (for example `whatsapp_status`). The MCP `initialize` response returns `{ server: "WhatsAppMCP", version: "3.0.x" }`. Compare against the version your release notes ship. This is the only check that proves the update reached the running process.

The same trace after the host restart

Here is what the trace looks like after you quit and relaunch the host. Notice the new PID and the version string that now matches the freshly published binary:

whatsapp_status after the host respawn

Library package vs. host-held CLI

Treating whatsapp-mcp-macos like a typical chalk or commander update is where most people get stuck. The differences are visible the moment you line them up side by side:

FeatureA typical npm library (e.g. chalk)whatsapp-mcp-macos (CLI held open by a host)
Target of the updateA .js file that the next `require()` call will loadA native binary already loaded into a running child process
Effect of `npm update`Next invocation sees the new codeNext invocation through the same host still sees the old code
What `npm list` reportsMatches what runs nextMatches disk; does not match the running process
Required restartNoneThe host (Claude Code, Cursor, launchd unit, PM2 app) must respawn the child
How to verifyRe-import or re-run the CLICall a tool and read `serverInfo.version` from the MCP initialize response
Failure mode if you skip the restartN/ASilent. No error. You get old behavior with a new version number on disk.

Installing whatsapp-mcp-macos for the first time?

There is a separate flow for fresh installs (Accessibility permission, MCP client config, postinstall prerequisites). Start at the setup guide.

Read the setup guide

Frequently asked questions

Does `npm update -g whatsapp-mcp-macos` apply instantly to my running Claude Code session?

No. MCP servers run as stdio child processes of their host. npm update replaces the binary on disk, but the already-spawned `whatsapp-mcp` process keeps the old executable mapped in memory. You must quit and relaunch the host (Claude Code, Cursor, Fazm) so it respawns the child. Until then, every tool call goes to the old binary with the old PID.

Why does `npm list -g whatsapp-mcp-macos` show the new version while the tools behave like the old one?

Because `npm list` reads package.json on disk; it has no idea what any parent process is still holding open. The npm tag in package.json moves the moment the update succeeds. The MCP server's own `serverInfo.version` (hardcoded in Sources/WhatsAppMCP/main.swift) only changes when a new process is spawned. Disk state and running state can legitimately drift.

How do I confirm the update reached the running MCP server?

Three checks, in order. (1) `npm list -g whatsapp-mcp-macos` confirms the tag on disk. (2) `stat -f '%Sm' $(which whatsapp-mcp)` confirms the compiled binary was rebuilt by postinstall. (3) In your MCP client, call `whatsapp_status`; the response includes `server`, `version`, and `pid`. A PID that predates your `npm update` command is the old binary, no matter what `npm list` says.

Why does the npm package publish version 1.1.0 but the server report version 3.0.0?

They count different things. The npm tag in package.json tracks the npm release (1.1.0). The `serverInfo.version` string hardcoded in Sources/WhatsAppMCP/main.swift tracks the MCP protocol surface the server exposes (3.0.0). A routine `npm update` always moves the first number. It only moves the second if the tagged release actually edited main.swift. This is why you cannot infer running behavior from the npm tag alone.

Will `npm update` kill my running host to force a respawn?

No, and it should not. npm has no concept of which processes currently hold open executables from its store. On POSIX systems, replacing a binary does not affect processes that already loaded it; the kernel keeps the old inode alive until the last fd closes. That is also why `rm $(which whatsapp-mcp)` during a live session would not crash Claude Code; the running server keeps executing from the unlinked inode.

Can I just send the MCP host a SIGHUP to reload the server?

MCP clients do not define a standard reload signal. Claude Code in particular restarts MCP servers on app relaunch, or when you toggle the server in settings. Cursor behaves the same way. The cleanest option is quit and relaunch the host. If you are self-hosting the server under launchd or PM2, restart the supervising unit instead (`launchctl kickstart -k gui/$UID/com.example.whatsapp-mcp` or `pm2 restart whatsapp-mcp`).

Does `npm update` rebuild the Swift binary every time?

Every version bump, yes. The package ships Swift source, and postinstall runs `xcrun swift build -c release`. SwiftPM caches dependencies in `~/Library/Developer/Xcode/DerivedData` and `~/.swiftpm`, so subsequent builds are fast. First-time builds after an Xcode upgrade fetch the MCP Swift SDK and MacosUseSDK again, which is the slow case.

What happens if I run `npm update` with `--ignore-scripts`?

You fetch the new tarball, skip postinstall, and end up with new Swift sources plus the OLD compiled binary under `.build/release/whatsapp-mcp`. Every generic npm tutorial suggests `--ignore-scripts` as a safer flag. For any package that compiles native code in postinstall, it produces a silently broken install. Use `npm rebuild -g whatsapp-mcp-macos` afterward to force the rebuild.

I updated and now macOS keeps asking me to re-grant Accessibility permission. Why?

Accessibility trust on macOS is keyed on the binary's path and code signature. A global update through the same Node version keeps the path identical (`…/lib/node_modules/whatsapp-mcp-macos/.build/release/whatsapp-mcp`), so the trust usually survives. If you switched Node version managers (Homebrew to nvm, or vice versa), the path changes and TCC treats it as a different app. Open System Settings > Privacy & Security > Accessibility and re-add the host.

Does `npm audit` cover the Swift dependencies pulled in by postinstall?

No. npm audit only walks npm's vulnerability database, which indexes JavaScript advisories. SwiftPM dependencies (MCP Swift SDK, MacosUseSDK) are invisible to it. For a package like whatsapp-mcp-macos the JS wrapper has zero production dependencies, so `npm audit` is essentially a no-op by design.