Skip to content

Plugin fails to load when host has libvips installed globally: sharp@0.34.5 postinstall picks source build, fails, leaves corrupt node_modules that traps wrapper recovery loop #102

@materemias

Description

@materemias

Plugin fails to load when host has libvips installed globally: sharp@0.34.5 postinstall picks source build, fails, leaves corrupt node_modules that traps wrapper recovery loop

Summary

episodic-memory@1.4.2 fails to load as a Claude Code plugin MCP server on hosts where libvips is installed globally (common on Arch Linux, many pkg-config-friendly distros, anything that bundles libvips for another package like GIMP/darktable/ImageMagick wrappers). The failure chain:

  1. Wrapper sees missing deps, runs npm install.
  2. sharp@0.34.5's postinstall (install/check.js) detects the host libvips and intentionally process.exit(1) to force a source build — even though the correct prebuilt @img/sharp-linux-x64@0.34.5 already exists as an optionalDependency and would work fine.
  3. The source-build fallback (npm run buildnode-gyp rebuild) fails immediately because sharp@0.34.5 doesn't declare node-addon-api as a runtime dependency. npm aborts the install.
  4. The aborted install leaves npm's .<pkg>-<hash>/ atomic-rename temp directories in node_modules/ — some with no package.json.
  5. Next launch the wrapper's findMissingDeps (added in Plugin fails to load on Windows: two upstream dep-resolution bugs slip past mcp-server-wrapper.js #95) detects the missing deps and retries npm install. This time arborist's canDedupe hits one of the empty temp dirs and crashes with TypeError: Invalid Version: (empty string fed to new SemVer()). Install fails with a different, more confusing error.
  6. The plugin stays in a stuck state across sessions until the corrupt node_modules is wiped by hand.

This is the same symptom class as #95 (partial node_modules → wrapper retry storm) and the same theme as #100 (native-dep postinstall broken on newer Node), but a distinct, separately filable root cause: sharp's useGlobalLibvips() heuristic on Linux hosts that happen to have libvips installed.

Environment

  • Plugin version: 1.4.2 (installed via superpowers-marketplace)
  • OS: Arch Linux, kernel 7.0.9-zen2-1-zen, x86_64
  • Node: v24.14.1
  • npm: 11.14.1
  • Host libvips: libvips 8.18.2 (installed via pacman as a dep of unrelated packages)
  • Plugin path: ~/.claude/plugins/cache/superpowers-marketplace/episodic-memory/1.4.2/

Step 1: sharp's install/check.js deliberately fails when global libvips is present

node_modules/sharp/install/check.js:

try {
  const { useGlobalLibvips } = require('../lib/libvips');
  if (useGlobalLibvips() || process.env.npm_config_build_from_source) {
    process.exit(1);
  }
} catch (err) {
  const summary = err.message.split(/\n/).slice(0, 1);
  console.log(`sharp: skipping install check: ${summary}`);
}

useGlobalLibvips() shells out to pkg-config --modversion vips-cpp and returns true if a sufficiently new libvips is present. On Arch with libvips 8.18.2 installed, it returns true, and check.js exits 1 by design.

That exit code is the trigger sharp uses to switch from "use the prebuilt platform-specific binary" to "build from source against the host libvips."

Step 2: Source build fails because sharp doesn't ship node-addon-api as a dep

sharp: Attempting to build from source via node-gyp
sharp: See https://sharp.pixelplumbing.com/install#building-from-source
sharp: Please add node-addon-api to your dependencies

sharp@0.34.5's dependencies:

{
  "@img/colour": "^1.0.0",
  "detect-libc": "^2.1.2",
  "semver": "^7.7.3"
}

No node-addon-api. The source-build path is non-functional out of the box. (This is arguably a sharp upstream bug, but it interacts badly with the next two steps and the fix lives just as well in episodic-memory.)

Step 3: Aborted install leaves corrupt node_modules

After npm aborts on sharp's failed build, node_modules/ contains npm's intermediate atomic-rename temp dirs that never got moved into place:

node_modules/
├── .base64-js-Sv0YhHFI/
├── .bl-G7t4Nums/
├── .buffer-xhltt17c/
├── .onnxruntime-common-jDSd4g7V/
├── ...
└── @anthropic-ai/
    ├── .claude-agent-sdk-linux-x64-SjyH8yzM/   # only contains `claude` binary, no package.json
    ├── .sdk-7uuiK8Wk/
    └── claude-agent-sdk/

Several of these dot-prefixed dirs are missing their package.json.

Step 4: Wrapper retry hits Invalid Version: in arborist

Next launch, findMissingDeps (#95) correctly detects deps are missing and runs npm install again. But arborist's canDedupe walks node_modules/.package-lock.json, encounters one of the empty temp dirs, and crashes:

npm error Invalid Version:
npm error
verbose stack TypeError: Invalid Version:
verbose stack     at new SemVer (/usr/lib/node_modules/semver/classes/semver.js:40:13)
verbose stack     at compare (/usr/lib/node_modules/semver/functions/compare.js:5:32)
verbose stack     at Object.eq (/usr/lib/node_modules/semver/functions/eq.js:4:29)
verbose stack     at Node.canDedupe (/usr/lib/node_modules/npm/node_modules/@npmcli/arborist/lib/node.js:1137:32)
verbose stack     at PlaceDep.pruneDedupable (/usr/lib/node_modules/npm/node_modules/@npmcli/arborist/lib/place-dep.js:426:14)
verbose stack     at new PlaceDep (/usr/lib/node_modules/npm/node_modules/@npmcli/arborist/lib/place-dep.js:278:14)
verbose stack     at #buildDepStep (...)

From the user's perspective the MCP server "just fails to connect" with no obvious clue that sharp is the trigger. Each subsequent session sees the same state and retries the same failing install.

Reproduction

On any Linux host with libvips installed system-wide (pacman -S libvips on Arch; equivalent on other distros):

PLUGIN=~/.claude/plugins/cache/superpowers-marketplace/episodic-memory/1.4.2

# Confirm pkg-config sees libvips
pkg-config --modversion vips-cpp     # → 8.18.2 or similar

# Fresh install attempt
rm -rf "$PLUGIN/node_modules" "$PLUGIN/package-lock.json"
cd "$PLUGIN" && npm install --no-audit --no-fund
# → fails: "sharp: Please add node-addon-api to your dependencies"
# → leaves dot-prefixed temp dirs in node_modules/

# Wrapper retry path
node "$PLUGIN/cli/mcp-server-wrapper.js" < /dev/null
# → "Missing dependencies under node_modules: onnxruntime-node"
# → triggers npm install
# → "npm error Invalid Version:"
# → wrapper exits 1, MCP shows × failed

Verification of the fix

Two ways to confirm the prebuilt path works for this user's platform:

# Option A: explicit env var (sharp documents this)
cd "$PLUGIN" && rm -rf node_modules package-lock.json
SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install --no-audit --no-fund
# → succeeds, uses @img/sharp-linux-x64 prebuilt

# Option B: skip postinstall scripts entirely
cd "$PLUGIN" && rm -rf node_modules package-lock.json
npm install --no-audit --no-fund --ignore-scripts
# → succeeds, @img/sharp-linux-x64 prebuilt installed as optionalDependency

# Either way, MCP boots:
node "$PLUGIN/cli/mcp-server-wrapper.js" < /dev/null
# → "Episodic Memory MCP server running via stdio"

@img/sharp-linux-x64@0.34.5 is already declared in sharp's optionalDependencies and downloads fine. There is no reason this user's platform should ever fall through to the source-build path — the prebuilt binary is the documented happy path for linux-x64-glibc.

Suggested direction (not prescriptive)

Pick whichever of these fits the project's taste:

  1. .npmrc in the plugin root with sharp_ignore_global_libvips=true. One line, persists across npm calls including the wrapper's recovery install, completely platform-neutral. Documented option from sharp itself: https://sharp.pixelplumbing.com/install#prebuilt-binaries. Lowest blast radius.

  2. Set SHARP_IGNORE_GLOBAL_LIBVIPS=1 in the env of the npm install call in mcp-server-wrapper.js. Same effect, scoped to the wrapper's recovery path. Doesn't affect anyone running npm install manually in the plugin dir.

  3. Pass --ignore-scripts to the wrapper's recovery npm install. Broader — also skips better-sqlite3's postinstall rebuild (relevant to postinstall npm rebuild better-sqlite3 is a silent no-op on Node 25 — binding never builds, recovery hint repeats the dead command #100). Combined with postinstall npm rebuild better-sqlite3 is a silent no-op on Node 25 — binding never builds, recovery hint repeats the dead command #100's instantiate-to-verify check, this would make the recovery path resilient to native-dep postinstall bugs in general, not just sharp.

  4. Add a one-time corruption cleanup before npm install in the wrapper. Walk node_modules/ for ^\.[a-zA-Z0-9_-]+-[A-Za-z0-9]{8}$ directories that lack a package.json and delete them before retry. Defensive, helps recover from any aborted install, not just sharp-triggered ones.

(1) and (4) together would make this fully self-healing on hosts that hit it for the first time and on hosts where it has already left corruption from a previous launch.

Why this is distinct from #95

#95's wrapper fix correctly probes findMissingDeps instead of trusting existsSync(node_modules). That probe now successfully detects the broken state caused by this issue. The follow-up npm install still fails — first because sharp re-triggers the same source-build path, second because the leftover temp dirs from the prior failed install poison arborist before sharp even gets called.

#95 closed the "missed broken state" gap. This issue is one specific way to create that broken state, with a separate fix that doesn't overlap.

Why this is distinct from #100

#100 is about better-sqlite3 postinstall being a silent no-op on Node 25 (so the install reports healthy but the binding never builds). This issue is the opposite shape: sharp's postinstall reports failure loudly (exit 1), npm aborts, no install completes at all. Different dep, different Node version, different failure mode — but both could share a single broader fix in the wrapper's recovery path (suggestion 3 above).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions