macOS: UtilityProcess rejects third-party native Node modules due to library validation (distinct from #180)
Summary
On macOS, Claude Desktop runs Node-based MCPB extensions inside an Electron UtilityProcess that enforces hardened-runtime library validation. Any bundled native .node binary not signed with Anthropic's Team ID is rejected by dlopen() before the extension's JS even starts executing. This breaks every MCPB that depends on a prebuilt native module: classic-level, better-sqlite3, lmdb, sharp, sqlite3, node-pty, fsevents, and so on.
This is distinct from #180 (which is about Node.js ABI / NODE_MODULE_VERSION mismatch). Library validation rejects the binary even when the ABI matches — the check happens before ABI checks.
The failure
Error observed in ~/Library/Logs/Claude/main.log (Claude Desktop 1.2581.0, macOS):
[UtilityProcess stderr] [nodeHost] import-failed: dlopen(
.../node_modules/classic-level/prebuilds/darwin-x64+arm64/classic-level.node,
0x0001
): code signature in <...> not valid for use in process:
mapping process and mapped file (non-platform) have different Team IDs
The process exits during module load before any console.error in the extension's entry point fires, which is why users see "Server disconnected" with no diagnostic output in mcp-server-*.log.
Reproduction
Minimal case: any MCPB manifest with server.type: "node" and a native dep whose prebuild ships an ad-hoc / linker-signed binary (i.e., every npm package using node-gyp-build prebuilds).
{
"server": {
"type": "node",
"entry_point": "dist/cli.js",
"mcp_config": {
"command": "node",
"args": ["${__dirname}/dist/cli.js"]
}
}
}
Where dist/cli.js does import "classic-level" (or any package with a prebuilt .node). Installed .mcpb, launched inside Claude Desktop → fails as above. The exact same code run via node dist/cli.js from a terminal works fine, because system Node has no hardened runtime.
Real-world instance affecting end users in my project: ignaciohermosillacornejo/copilot-money-mcp#249.
Root cause
macOS hardened runtime requires every dynamically loaded library to share the host process's Team ID (Anthropic's is Q6L2SF6YDW) unless the host process carries the entitlement com.apple.security.cs.disable-library-validation.
Inspecting Claude.app's helper entitlements (codesign -d --entitlements -):
| Helper |
disable-library-validation |
| Claude (main) |
❌ |
| Helper (GPU) |
❌ |
| Helper (Plugin) |
✅ |
| Helper (Renderer) |
❌ |
| Helper.app |
❌ |
Only "Helper (Plugin).app" carries the entitlement, but Claude Desktop's MCP UtilityProcess does not run inside it. Electron exposes a macOS-only flag on utilityProcess.fork(), allowLoadingUnsignedLibraries: true, which routes the forked process to the Plugin helper. Passing this flag — combined with ensuring the Plugin helper is signed with disable-library-validation — is the standard fix for plugin-hosting Electron apps.
Anthropic's own native modules (sharp-darwin-arm64.node, audio-capture.node, computer_use.node in app.asar.unpacked/) load fine because they're signed with Anthropic's certificate.
Prior art
- LM Studio hit the identical bug in February 2026: lmstudio-ai/lmstudio-bug-tracker#1494. Root cause was the Plugin helper missing
disable-library-validation. Fixed with a one-line entitlement addition.
- VS Code, Cursor, and Obsidian all sign their plugin-hosting helpers with
disable-library-validation and pass allowLoadingUnsignedLibraries: true from the main process. This is standard practice for Electron apps that load third-party native code.
- The MCPB README already warns: "Limitation: Cannot portably bundle compiled dependencies." This issue is the macOS-specific mechanism behind that limitation; there is no documentation or official workaround for it today.
Suggested fix
- Sign
Claude Helper (Plugin).app with an entitlements plist that includes com.apple.security.cs.disable-library-validation.
- In the Claude Desktop MCP host, call
utilityProcess.fork(modulePath, args, { allowLoadingUnsignedLibraries: true, ... }) when launching Node-based MCPB extensions on macOS.
These two changes together allow third-party .node prebuilds to load without breaking anything else in the hardened runtime model (JIT restrictions etc. still apply to the main/renderer/GPU helpers).
Current workaround for extension authors
Until the entitlement is added, extensions can escape the UtilityProcess path by setting mcp_config.command to something other than the literal string "node" — e.g. /usr/bin/env with node prepended to args, scoped to darwin via platform_overrides. This causes the router in app.asar (.vite/build/index.js) to fall through to {type: "exec"}, which spawns the server as a plain child process of the Claude Desktop main process (no hardened runtime), where third-party .node binaries load normally.
"mcp_config": {
"command": "node",
"args": ["${__dirname}/dist/cli.js"],
"platform_overrides": {
"darwin": {
"command": "/usr/bin/env",
"args": ["node", "${__dirname}/dist/cli.js"]
}
}
}
This works today but relies on an undocumented routing detail and is not something extension authors should have to discover by reverse-engineering app.asar. It also depends on node being resolvable from Claude Desktop's (non-inherited) PATH, which is fragile across environments (nvm, homebrew-arm64, etc.).
Impact
Every MCPB that wants to use a native Node module on macOS is currently broken unless the author stumbles onto the workaround. This includes common dependencies: any SQLite/LevelDB/LMDB backend, image processing (sharp), filesystem watchers (chokidar/fsevents), PTY (node-pty), and many more. The practical outcome is that MCPB authors are forced to either (a) avoid native deps entirely, (b) ship as npx-based MCP servers in claude_desktop_config.json instead of MCPB, or (c) use the undocumented workaround.
Environment
- Claude Desktop: 1.2581.0 (reproduced); reporters on older 1.2xxx versions also hit it
- macOS: Darwin 24.6.0 (Sequoia), arm64
- Example package triggering it:
classic-level@^2.0 (same applies to any node-gyp-build prebuild)
Happy to provide additional logs, a minimal reproducer repo, or help verify a fix.
macOS: UtilityProcess rejects third-party native Node modules due to library validation (distinct from #180)
Summary
On macOS, Claude Desktop runs Node-based MCPB extensions inside an Electron
UtilityProcessthat enforces hardened-runtime library validation. Any bundled native.nodebinary not signed with Anthropic's Team ID is rejected bydlopen()before the extension's JS even starts executing. This breaks every MCPB that depends on a prebuilt native module:classic-level,better-sqlite3,lmdb,sharp,sqlite3,node-pty,fsevents, and so on.This is distinct from #180 (which is about Node.js ABI /
NODE_MODULE_VERSIONmismatch). Library validation rejects the binary even when the ABI matches — the check happens before ABI checks.The failure
Error observed in
~/Library/Logs/Claude/main.log(Claude Desktop 1.2581.0, macOS):The process exits during module load before any
console.errorin the extension's entry point fires, which is why users see "Server disconnected" with no diagnostic output inmcp-server-*.log.Reproduction
Minimal case: any MCPB manifest with
server.type: "node"and a native dep whose prebuild ships an ad-hoc / linker-signed binary (i.e., every npm package usingnode-gyp-buildprebuilds).{ "server": { "type": "node", "entry_point": "dist/cli.js", "mcp_config": { "command": "node", "args": ["${__dirname}/dist/cli.js"] } } }Where
dist/cli.jsdoesimport "classic-level"(or any package with a prebuilt.node). Installed .mcpb, launched inside Claude Desktop → fails as above. The exact same code run vianode dist/cli.jsfrom a terminal works fine, because system Node has no hardened runtime.Real-world instance affecting end users in my project: ignaciohermosillacornejo/copilot-money-mcp#249.
Root cause
macOS hardened runtime requires every dynamically loaded library to share the host process's Team ID (Anthropic's is Q6L2SF6YDW) unless the host process carries the entitlement
com.apple.security.cs.disable-library-validation.Inspecting Claude.app's helper entitlements (
codesign -d --entitlements -):disable-library-validationOnly "Helper (Plugin).app" carries the entitlement, but Claude Desktop's MCP
UtilityProcessdoes not run inside it. Electron exposes a macOS-only flag onutilityProcess.fork(),allowLoadingUnsignedLibraries: true, which routes the forked process to the Plugin helper. Passing this flag — combined with ensuring the Plugin helper is signed withdisable-library-validation— is the standard fix for plugin-hosting Electron apps.Anthropic's own native modules (
sharp-darwin-arm64.node,audio-capture.node,computer_use.nodeinapp.asar.unpacked/) load fine because they're signed with Anthropic's certificate.Prior art
disable-library-validation. Fixed with a one-line entitlement addition.disable-library-validationand passallowLoadingUnsignedLibraries: truefrom the main process. This is standard practice for Electron apps that load third-party native code.Suggested fix
Claude Helper (Plugin).appwith an entitlements plist that includescom.apple.security.cs.disable-library-validation.utilityProcess.fork(modulePath, args, { allowLoadingUnsignedLibraries: true, ... })when launching Node-based MCPB extensions on macOS.These two changes together allow third-party
.nodeprebuilds to load without breaking anything else in the hardened runtime model (JIT restrictions etc. still apply to the main/renderer/GPU helpers).Current workaround for extension authors
Until the entitlement is added, extensions can escape the
UtilityProcesspath by settingmcp_config.commandto something other than the literal string"node"— e.g./usr/bin/envwithnodeprepended toargs, scoped todarwinviaplatform_overrides. This causes the router inapp.asar(.vite/build/index.js) to fall through to{type: "exec"}, which spawns the server as a plain child process of the Claude Desktop main process (no hardened runtime), where third-party.nodebinaries load normally.This works today but relies on an undocumented routing detail and is not something extension authors should have to discover by reverse-engineering
app.asar. It also depends onnodebeing resolvable from Claude Desktop's (non-inherited) PATH, which is fragile across environments (nvm, homebrew-arm64, etc.).Impact
Every MCPB that wants to use a native Node module on macOS is currently broken unless the author stumbles onto the workaround. This includes common dependencies: any SQLite/LevelDB/LMDB backend, image processing (sharp), filesystem watchers (chokidar/fsevents), PTY (node-pty), and many more. The practical outcome is that MCPB authors are forced to either (a) avoid native deps entirely, (b) ship as
npx-based MCP servers inclaude_desktop_config.jsoninstead of MCPB, or (c) use the undocumented workaround.Environment
classic-level@^2.0(same applies to anynode-gyp-buildprebuild)Happy to provide additional logs, a minimal reproducer repo, or help verify a fix.