Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"plugin/scripts/*.cjs",
"plugin/skills",
"plugin/ui",
"plugin/node_modules",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 plugin/node_modules includes platform-specific native binaries

Including plugin/node_modules in files means the published tarball will contain the tree-sitter grammar .node prebuilt binaries compiled for the platform on which npm run build was run (macOS arm64, x64, Linux, etc.). Users who install the package on a different platform will silently get non-functional grammars even though they would otherwise succeed with an on-machine install. Since the grammars are optionalDependencies, this degrades code-graph parsing rather than causing a crash, but it is worth being aware that each publish will be inadvertently platform-scoped for the optional features. Depending on the size of the grammar binaries (each can be several MB), the tarball size may also increase substantially — worth running npm pack --dry-run to verify the delta before releasing.

Prompt To Fix With AI
This is a comment left during a code review.
Path: package.json
Line: 59

Comment:
**`plugin/node_modules` includes platform-specific native binaries**

Including `plugin/node_modules` in `files` means the published tarball will contain the tree-sitter grammar `.node` prebuilt binaries compiled for the platform on which `npm run build` was run (macOS arm64, x64, Linux, etc.). Users who install the package on a different platform will silently get non-functional grammars even though they would otherwise succeed with an on-machine install. Since the grammars are `optionalDependencies`, this degrades code-graph parsing rather than causing a crash, but it is worth being aware that each publish will be inadvertently platform-scoped for the optional features. Depending on the size of the grammar binaries (each can be several MB), the tarball size may also increase substantially — worth running `npm pack --dry-run` to verify the delta before releasing.

How can I resolve this? If you propose a fix, please make it concise.

"openclaw"
],
"engines": {
Expand Down
48 changes: 48 additions & 0 deletions plugin/scripts/bun-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { existsSync, readFileSync, mkdirSync, appendFileSync, writeFileSync } fr
import { join, dirname, resolve } from 'path';
import { homedir } from 'os';
import { fileURLToPath } from 'url';
import { createRequire } from 'module';

const IS_WINDOWS = process.platform === 'win32';

Expand Down Expand Up @@ -101,6 +102,53 @@ if (!bunPath) {
process.exit(1);
}

// Runtime self-heal: ensure the worker's externalized deps are present in
// plugin/node_modules before we spawn it. The build-time install + tarball
// bundling (build-hooks.js + package.json `files`) covers the npm channel,
// but the MARKETPLACE channel is a `git clone` of this repo where
// `plugin/node_modules` is gitignored and never committed — so a freshly
// installed marketplace plugin has no node_modules and every hook crashes
// with `Cannot find module 'zod/v3'` (issues #2407 / #2453 / #2640 / #2379).
// We can't fix that at build time (the install output is gitignored), so we
// heal once here, on first run, before the worker is invoked.
function ensureRuntimeDeps() {
let pkgJsonPath;
try {
pkgJsonPath = join(RESOLVED_PLUGIN_ROOT, 'package.json');
if (!existsSync(pkgJsonPath)) return; // not a plugin root with deps
const pluginRequire = createRequire(pkgJsonPath);
pluginRequire.resolve('zod/v3'); // resolves → deps present, nothing to do
return;
} catch {
// zod/v3 unresolvable → install the hook-critical deps once.
// We install ONLY zod + shell-quote (the pure-JS externals the worker
// needs to boot), with --ignore-scripts. Rationale: npm resolves the full
// dep tree from the existing package.json, and the tree-sitter grammars
// are native node-gyp builds — on a Node version without a prebuilt
// binding (e.g. Node 26) a grammar build fails and aborts the whole
// install, leaving zod uninstalled. --ignore-scripts skips those native
// postinstalls (zod/shell-quote are pure JS and need none), so the hook
// always recovers. Grammar/code-graph deps heal separately via the full
// `npx claude-mem install` and are not required for hooks to run.
console.error('[bun-runner] plugin/node_modules missing zod — installing hook-critical deps (first run on this install)...');
const install = spawnSync('npm', ['install', '--no-save', '--no-audit', '--no-fund', '--ignore-scripts', 'zod@^4.3.6', 'shell-quote@^1.8.3'], {
cwd: RESOLVED_PLUGIN_ROOT,
stdio: ['ignore', 'inherit', 'inherit'],
// npm on Windows is a .cmd shim — spawn without shell hits ENOENT.
shell: IS_WINDOWS,
});
Comment on lines +134 to +139

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 npm install stdout contaminates hook's structured output

stdio: ['ignore', 'inherit', 'inherit'] passes npm's stdout straight to bun-runner.js's own stdout, which Claude Code reads as the hook's response. For UserPromptSubmit (and any hook that must return structured JSON on stdout), npm's "added N packages in Xs" summary line — which npm v9/v10 write to stdout, not stderr, even in a non-TTY pipe — would appear before the hook's JSON and cause Claude Code to fail to parse the response. The failure occurs only on first run on a marketplace install (exactly when the fix is most needed), and silently corrupts the hook decision.

Redirect index 1 of stdio to 'pipe' or 'ignore' so npm output stays off the hook's stdout channel.

Prompt To Fix With AI
This is a comment left during a code review.
Path: plugin/scripts/bun-runner.js
Line: 126-131

Comment:
**npm install stdout contaminates hook's structured output**

`stdio: ['ignore', 'inherit', 'inherit']` passes npm's stdout straight to bun-runner.js's own stdout, which Claude Code reads as the hook's response. For `UserPromptSubmit` (and any hook that must return structured JSON on stdout), npm's "added N packages in Xs" summary line — which npm v9/v10 write to **stdout**, not stderr, even in a non-TTY pipe — would appear before the hook's JSON and cause Claude Code to fail to parse the response. The failure occurs only on first run on a marketplace install (exactly when the fix is most needed), and silently corrupts the hook decision.

Redirect index 1 of stdio to `'pipe'` or `'ignore'` so npm output stays off the hook's stdout channel.

How can I resolve this? If you propose a fix, please make it concise.

if (install.error) {
console.error(`[bun-runner] could not run npm install in ${RESOLVED_PLUGIN_ROOT}: ${install.error.message}`);
} else if (install.status === 0) {
console.error('[bun-runner] runtime deps installed.');
} else {
console.error(`[bun-runner] npm install exited with code ${install.status}. Run \`cd ${RESOLVED_PLUGIN_ROOT} && npm install\` manually.`);
}
}
}

ensureRuntimeDeps();

function collectStdin() {
return new Promise((resolve) => {
if (process.stdin.isTTY) {
Expand Down
65 changes: 64 additions & 1 deletion scripts/build-hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,19 @@ async function buildHooks() {
private: true,
description: 'Runtime dependencies for claude-mem bundled hooks',
type: 'module',
// Hook-critical deps that MUST install for the worker to boot. Kept tiny
// and pure-JS so `npm install` can never fail on a native build here.
dependencies: {
'zod': '^4.4.3',
'shell-quote': '^1.8.3',
},
// Tree-sitter grammars need node-gyp / prebuild-install. On a Node version
// with no prebuilt binding (e.g. Node 26) the native build fails — and as
// hard `dependencies` that aborts the ENTIRE install, taking zod down with
// it and crashing every hook (issues #2407 / #2453 / #2640 / #2379).
// As optionalDependencies a grammar build failure is tolerated: zod still
// installs, hooks still boot, and only code-graph parsing degrades.
optionalDependencies: {
'tree-sitter-cli': '^0.26.5',
'tree-sitter-c': '^0.24.1',
'tree-sitter-cpp': '^0.23.4',
Expand All @@ -256,7 +267,6 @@ async function buildHooks() {
'@tree-sitter-grammars/tree-sitter-yaml': '^0.7.1',
'@derekstride/tree-sitter-sql': '^0.3.11',
'@tree-sitter-grammars/tree-sitter-markdown': '^0.3.2',
'shell-quote': '^1.8.3',
},
overrides: {
'tree-sitter': '^0.25.0'
Expand All @@ -272,6 +282,40 @@ async function buildHooks() {
fs.writeFileSync('plugin/package.json', JSON.stringify(pluginPackageJson, null, 2) + '\n');
console.log('✓ plugin/package.json generated');

// Populate plugin/node_modules so the bundled worker can resolve its
// externalized runtime deps (zod/v3, shell-quote, tree-sitter grammars).
// The worker bundle marks these `external`, so without an installed
// plugin/node_modules every hook crashes at runtime with
// `Cannot find module 'zod/v3'` (issues #2407 / #2453 / #2640 / #2379).
//
// The result must also ship: the `plugin/node_modules` entry in root
// package.json `files` carries it into the npm tarball (npm only
// force-strips a *top-level* node_modules, not a nested one named
// explicitly in `files` — verified via `npm pack`).
//
// npm (not bun) so CI/publishers without bun still work. We do NOT pass
// --ignore-scripts: tree-sitter grammars use prebuild-install postinstalls
// to fetch prebuilt .node bindings. Install is fail-soft (grammars are
// optionalDependencies); the resolve gate near the end of this build is
// the hard guard that zod/v3 actually landed.
console.log('\n📦 Installing plugin runtime dependencies into plugin/node_modules...');
{
const { spawnSync: spawnInstall } = await import('node:child_process');
const installResult = spawnInstall('npm', ['install', '--omit=dev', '--no-audit', '--no-fund'], {
cwd: path.join(__dirname, '..', 'plugin'),
stdio: 'inherit',
// npm on Windows is a .cmd shim — spawn without shell hits ENOENT.
shell: process.platform === 'win32',
});
if (installResult.error) {
console.warn(`⚠️ Could not invoke npm in plugin/: ${installResult.error.message}. Run \`cd plugin && npm install\` manually.`);
} else if (installResult.status === 0) {
console.log('✓ plugin/node_modules populated');
} else {
console.warn(`⚠️ npm install in plugin/ exited with code ${installResult.status}. Run \`cd plugin && npm install\` manually.`);
}
}

console.log('\n📋 Building React viewer...');
const { spawn } = await import('child_process');
const viewerBuild = spawn('node', ['scripts/build-viewer.js'], { stdio: 'inherit' });
Expand Down Expand Up @@ -662,6 +706,25 @@ async function buildHooks() {

await verifyShellTemplateCanonical();

// Hard gate: prove the worker's externalized deps actually resolve from
// plugin/node_modules. The install step above is fail-soft, so this is the
// real guard against shipping a tarball that crashes every hook with
// `Cannot find module 'zod/v3'` (issues #2407 / #2453 / #2640 / #2379).
console.log('\n📋 Verifying plugin runtime deps resolve...');
{
const { createRequire } = await import('node:module');
const pluginRequire = createRequire(path.join(__dirname, '..', 'plugin', 'package.json'));
const criticalRuntimeDeps = ['zod/v3', 'shell-quote'];
for (const dep of criticalRuntimeDeps) {
try {
pluginRequire.resolve(dep);
} catch {
throw new Error(`Plugin runtime dep '${dep}' does not resolve from plugin/node_modules — the bundled worker would crash at runtime. Run \`cd plugin && npm install\` and rebuild.`);
}
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}
console.log('✓ Plugin runtime deps resolve (zod/v3, shell-quote)');

console.log('\n✅ All build targets compiled successfully!');
console.log(` Output: ${hooksDir}/`);
console.log(` - Worker: worker-service.cjs`);
Expand Down