Skip to content

postinstall npm rebuild better-sqlite3 is a silent no-op on Node 25 — binding never builds, recovery hint repeats the dead command #100

@bgilly

Description

@bgilly

Summary

scripts/postinstall.js rebuilds the native binding with npm rebuild better-sqlite3. On Node 25 (macOS arm64, better-sqlite3 12.10.0) that command is a silent no-op: it prints rebuilt dependencies successfully, exits 0, and produces no build/Release/better_sqlite3.node. So a fresh plugin install / update ships with no loadable binding, every SessionStart sync dies with Could not locate the bindings file, and the MCP search/read tools return nothing.

This is the install-side root cause underneath #94. #94 is about the failure being invisible and explicitly assumes npm rebuild better-sqlite3 is the working local fix — but that reporter was on Node 22 / Linux. On Node 25 the rebuild command itself doesn't work, so the postinstall can never succeed and the recovery hint it prints is dead.

Environment

  • Plugin version: 1.4.2 (also reproduced against the 1.4.2 node_modules)
  • Plugin source: superpowers-marketplace
  • OS: macOS (Darwin 25.5.0), arm64
  • Node: v25.2.1 (NODE_MODULE_VERSION 141)
  • better-sqlite3: 12.10.0
  • Xcode CLT + python3 present (node-gyp can build)

Root cause (empirical)

better-sqlite3's own install script is prebuild-install || node-gyp rebuild --release. The postinstall doesn't invoke that — it invokes npm rebuild better-sqlite3, and on this setup npm rebuild never runs the package's install script:

$ rm -f node_modules/better-sqlite3/build/Release/better_sqlite3.node

$ npm rebuild better-sqlite3 --foreground-scripts
rebuilt dependencies successfully           # exit 0

$ ls node_modules/better-sqlite3/build/Release/better_sqlite3.node
ls: ...better_sqlite3.node: No such file or directory   # nothing built

The two commands that DO produce a working binding, run directly in the better-sqlite3 dir:

$ ./node_modules/.bin/prebuild-install      # places a prebuilt, exit 0
# or
$ npm run build-release                     # = node-gyp rebuild --release, compiles locally

After npm run build-release, new Database(':memory:') loads and queries fine.

Why postinstall.js can't catch it

const result = spawnSync(npmBin, ['rebuild', 'better-sqlite3'], { ... });
if (result.status !== 0) { /* warn */ }

npm rebuild's no-op exits 0, so status !== 0 is never true — the failure is undetectable by exit code. The warning branch (which prints Recover with: cd <plugin-dir> && npm rebuild better-sqlite3) both never fires here AND recommends the same command that doesn't work on Node 25.

A second trap for anyone writing a health check: require('better-sqlite3') succeeds even with no binding, because better-sqlite3 loads the native addon lazily inside new Database(). So a require()-based preflight passes on a broken install. The binding only actually loads when a Database is instantiated.

Suggested direction (not prescriptive)

The robust check is "did the binding actually load," not "did the rebuild command exit 0":

  1. In postinstall, after the rebuild, verify by instantiating new (require('better-sqlite3'))(':memory:') in a child process. If it throws, fall back to node-gyp rebuild --release (or run the package's own install script / prebuild-install) and re-verify.
  2. Prefer invoking better-sqlite3's actual install path (prebuild-install || node-gyp rebuild --release) over npm rebuild, which (at least on npm shipped with Node 25) doesn't run that package's install script.
  3. Pairs naturally with SessionStart sync failures are invisible: npm rebuild ... 2>/dev/null || true + --background hook hide native-binding errors from users #94: the same instantiate-to-verify check, run in the SessionStart hook, surfaces a real remediation instead of a silent log line.

Reproduction

PLUGIN=~/.claude/plugins/cache/superpowers-marketplace/episodic-memory/1.4.2
rm -f "$PLUGIN/node_modules/better-sqlite3/build/Release/better_sqlite3.node"
( cd "$PLUGIN/node_modules/better-sqlite3" && npm rebuild better-sqlite3 )   # "success", builds nothing
node -e "new (require('$PLUGIN/node_modules/better-sqlite3'))(':memory:')"   # Could not locate the bindings file
( cd "$PLUGIN/node_modules/better-sqlite3" && npm run build-release )        # this actually builds it

Prior art checked

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