From ad3d236cec01988d6ee38ecaac1cf44bf2261196 Mon Sep 17 00:00:00 2001 From: Alex Newman Date: Tue, 24 Feb 2026 19:31:26 -0500 Subject: [PATCH] fix: resolve hook crashes and CLAUDE_PLUGIN_ROOT fallback (#1215, #1220) (#1229) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: resolve PostToolUse hook crashes and 5s latency (#1220) Three compounding bugs caused hook failures: 1. Missing break statements in worker-service.ts switch — if async code threw before process.exit(), execution fell through to subsequent cases. Added break to all 7 cases missing them. 2. Unhandled promise rejection on main() — added .catch() that logs the error and exits 0 (per project exit code strategy: don't block Claude Code or leave Windows Terminal tabs open). 3. Redundant start commands in hooks.json — PostToolUse, UserPromptSubmit, and Stop groups each had a standalone start command that was redundant (the hook case already calls ensureWorkerStarted internally). The redundant start also caused 5s latency via bun-runner.js collectStdin() timeout since Claude Code never closes stdin. Co-Authored-By: Claude Opus 4.6 * fix: add CLAUDE_PLUGIN_ROOT fallback for Stop hooks (#1215) Upstream Claude Code bug (anthropics/claude-code#24529) leaves CLAUDE_PLUGIN_ROOT unset for Stop hooks on macOS and ALL hooks on Linux. Two-layer defense: 1. Shell-level: hooks.json commands now use inline fallback _R="${CLAUDE_PLUGIN_ROOT}"; [ -z "$_R" ] && _R="$HOME/..."; falling back to the known marketplace install path. 2. Script-level: bun-runner.js self-resolves plugin root from its own filesystem location via import.meta.url, and fixes broken /scripts/... paths that result from empty expansion. Added test to verify all hook commands include the fallback path. Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- plugin/hooks/hooks.json | 31 +++++-------------- plugin/scripts/bun-runner.js | 30 +++++++++++++++++- plugin/scripts/worker-service.cjs | 2 +- src/services/worker-service.ts | 12 ++++++- .../plugin-distribution.test.ts | 16 ++++++++++ 5 files changed, 65 insertions(+), 26 deletions(-) diff --git a/plugin/hooks/hooks.json b/plugin/hooks/hooks.json index d1e9c626..6f044d9b 100644 --- a/plugin/hooks/hooks.json +++ b/plugin/hooks/hooks.json @@ -7,7 +7,7 @@ "hooks": [ { "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/scripts/setup.sh", + "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; \"$_R/scripts/setup.sh\"", "timeout": 300 } ] @@ -19,7 +19,7 @@ "hooks": [ { "type": "command", - "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/smart-install.js\"", + "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/smart-install.js\"", "timeout": 300 } ] @@ -29,12 +29,12 @@ "hooks": [ { "type": "command", - "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" start", + "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" start", "timeout": 60 }, { "type": "command", - "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code context", + "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" hook claude-code context", "timeout": 60 } ] @@ -45,12 +45,7 @@ "hooks": [ { "type": "command", - "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" start", - "timeout": 60 - }, - { - "type": "command", - "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code session-init", + "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" hook claude-code session-init", "timeout": 60 } ] @@ -62,12 +57,7 @@ "hooks": [ { "type": "command", - "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" start", - "timeout": 60 - }, - { - "type": "command", - "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code observation", + "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" hook claude-code observation", "timeout": 120 } ] @@ -78,17 +68,12 @@ "hooks": [ { "type": "command", - "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" start", - "timeout": 60 - }, - { - "type": "command", - "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code summarize", + "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" hook claude-code summarize", "timeout": 120 }, { "type": "command", - "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code session-complete", + "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" hook claude-code session-complete", "timeout": 30 } ] diff --git a/plugin/scripts/bun-runner.js b/plugin/scripts/bun-runner.js index 0de7c876..90ee0997 100644 --- a/plugin/scripts/bun-runner.js +++ b/plugin/scripts/bun-runner.js @@ -13,11 +13,36 @@ */ import { spawnSync, spawn } from 'child_process'; import { existsSync, readFileSync } from 'fs'; -import { join } from 'path'; +import { join, dirname, resolve } from 'path'; import { homedir } from 'os'; +import { fileURLToPath } from 'url'; const IS_WINDOWS = process.platform === 'win32'; +// Self-resolve plugin root when CLAUDE_PLUGIN_ROOT is not set by Claude Code. +// Upstream bug: anthropics/claude-code#24529 — Stop hooks (and on Linux, all hooks) +// don't receive CLAUDE_PLUGIN_ROOT, causing script paths to resolve to /scripts/... +// which doesn't exist. This fallback derives the plugin root from bun-runner.js's +// own filesystem location (this file lives in /scripts/). +const __bun_runner_dirname = dirname(fileURLToPath(import.meta.url)); +const RESOLVED_PLUGIN_ROOT = process.env.CLAUDE_PLUGIN_ROOT || resolve(__bun_runner_dirname, '..'); + +/** + * Fix script path arguments that were broken by empty CLAUDE_PLUGIN_ROOT. + * When CLAUDE_PLUGIN_ROOT is empty, "${CLAUDE_PLUGIN_ROOT}/scripts/foo.cjs" + * expands to "/scripts/foo.cjs" which doesn't exist. Detect this and rewrite + * the path using our self-resolved plugin root. + */ +function fixBrokenScriptPath(argPath) { + if (argPath.startsWith('/scripts/') && !existsSync(argPath)) { + const fixedPath = join(RESOLVED_PLUGIN_ROOT, argPath); + if (existsSync(fixedPath)) { + return fixedPath; + } + } + return argPath; +} + /** * Find Bun executable - checks PATH first, then common install locations */ @@ -80,6 +105,9 @@ if (args.length === 0) { process.exit(1); } +// Fix broken script paths caused by empty CLAUDE_PLUGIN_ROOT (#1215) +args[0] = fixBrokenScriptPath(args[0]); + const bunPath = findBun(); if (!bunPath) { diff --git a/plugin/scripts/worker-service.cjs b/plugin/scripts/worker-service.cjs index ff5e4f49..4ec18e9e 100755 --- a/plugin/scripts/worker-service.cjs +++ b/plugin/scripts/worker-service.cjs @@ -1444,7 +1444,7 @@ Tips: SET status = 'failed', failed_at_epoch = ? WHERE status = 'pending' AND session_db_id IN (${d}) - `).run(Date.now(),...l);p.changes>0&&w.info("SYSTEM",`Marked ${p.changes} pending messages from stale sessions as failed`)}}catch(u){w.error("SYSTEM","Failed to clean up stale sessions",{},u)}let a=n.getSessionsWithPendingMessages(),c={totalPendingSessions:a.length,sessionsStarted:0,sessionsSkipped:0,startedSessionIds:[]};if(a.length===0)return c;w.info("SYSTEM",`Processing up to ${e} of ${a.length} pending session queues`);for(let u of a){if(c.sessionsStarted>=e)break;try{if(this.sessionManager.getSession(u)?.generatorPromise){c.sessionsSkipped++;continue}let d=this.sessionManager.initializeSession(u);w.info("SYSTEM",`Starting processor for session ${u}`,{project:d.project,pendingCount:n.getPendingCount(u)}),this.startSessionProcessor(d,"startup-recovery"),c.sessionsStarted++,c.startedSessionIds.push(u),await new Promise(p=>setTimeout(p,100))}catch(l){w.error("SYSTEM",`Failed to process session ${u}`,{},l),c.sessionsSkipped++}}return c}async shutdown(){this.stopOrphanReaper&&(this.stopOrphanReaper(),this.stopOrphanReaper=null),this.staleSessionReaperInterval&&(clearInterval(this.staleSessionReaperInterval),this.staleSessionReaperInterval=null),await xA({server:this.server.getHttpServer(),sessionManager:this.sessionManager,mcpClient:this.mcpClient,dbManager:this.dbManager,chromaMcpManager:this.chromaMcpManager||void 0})}broadcastProcessingStatus(){let e=this.sessionManager.isAnySessionProcessing(),r=this.sessionManager.getTotalActiveWork(),n=this.sessionManager.getActiveSessionCount();w.info("WORKER","Broadcasting processing status",{isProcessing:e,queueDepth:r,activeSessions:n}),this.sseBroadcaster.broadcast({type:"processing_status",isProcessing:e,queueDepth:r})}};async function hF(t){if(hA(),await Ka(t,1e3)){let s=await SA(t);if(s.matches)return w.info("SYSTEM","Worker already running and healthy"),!0;if(fA(15e3)){if(w.info("SYSTEM","Version mismatch detected but PID file is recent \u2014 another restart likely in progress, polling health",{pluginVersion:s.pluginVersion,workerVersion:s.workerVersion}),await Ka(t,15e3))return w.info("SYSTEM","Worker became healthy after waiting for concurrent restart"),!0;w.warn("SYSTEM","Worker did not become healthy after waiting \u2014 proceeding with own restart")}if(w.info("SYSTEM","Worker version mismatch detected - auto-restarting",{pluginVersion:s.pluginVersion,workerVersion:s.workerVersion}),await qm(t),!await Lm(t,Os(Cr.PORT_IN_USE_WAIT)))return w.error("SYSTEM","Port did not free up after shutdown for version mismatch restart",{port:t}),!1;Yn()}return await Wa(t)?(w.info("SYSTEM","Port in use, waiting for worker to become healthy"),await Ka(t,Os(Cr.PORT_IN_USE_WAIT))?(w.info("SYSTEM","Worker is now healthy"),!0):(w.error("SYSTEM","Port in use but worker not responding to health checks"),!1)):zge()?(w.warn("SYSTEM","Worker unavailable on Windows \u2014 skipping spawn (recent attempt failed within cooldown)"),!1):(w.info("SYSTEM","Starting worker daemon"),Uge(),D0(__filename,t)===void 0?(w.error("SYSTEM","Failed to spawn worker daemon"),!1):await Ka(t,Os(Cr.POST_SPAWN_WAIT))?(await bA(t,Os(Cr.READINESS_WAIT))||w.warn("SYSTEM","Worker is alive but readiness timed out \u2014 proceeding anyway"),Lge(),mA(),w.info("SYSTEM","Worker started successfully"),!0):(Yn(),w.error("SYSTEM","Worker failed to start (health check timeout)"),!1))}async function Fge(){let t=process.argv[2];(["start","hook","restart","--daemon"].includes(t)||t===void 0)&&jm()&&process.exit(0);let r=Et();function n(i,s){let o=gF(i,s);console.log(JSON.stringify(o)),process.exit(0)}switch(t){case"start":await hF(r)?n("ready"):n("error","Failed to start worker");case"stop":await qm(r),await Lm(r,Os(15e3))||w.warn("SYSTEM","Port did not free up after shutdown",{port:r}),Yn(),w.info("SYSTEM","Worker stopped successfully"),process.exit(0);case"restart":w.info("SYSTEM","Restarting worker"),await qm(r),await Lm(r,Os(15e3))||(w.error("SYSTEM","Port did not free up after shutdown, aborting restart",{port:r}),process.exit(0)),Yn(),D0(__filename,r)===void 0&&(w.error("SYSTEM","Failed to spawn worker daemon during restart"),process.exit(0)),await Ka(r,Os(Cr.POST_SPAWN_WAIT))||(Yn(),w.error("SYSTEM","Worker failed to restart"),process.exit(0)),w.info("SYSTEM","Worker restarted successfully"),process.exit(0);case"status":{let i=await Wa(r),s=Um();i&&s?(console.log("Worker is running"),console.log(` PID: ${s.pid}`),console.log(` Port: ${s.port}`),console.log(` Started: ${s.startedAt}`)):console.log("Worker is not running"),process.exit(0)}case"cursor":{let i=process.argv[3],s=await Z2(i,process.argv.slice(4));process.exit(s)}case"hook":{await hF(r)||w.warn("SYSTEM","Worker failed to start before hook, handler will retry");let s=process.argv[3],o=process.argv[4];(!s||!o)&&(console.error("Usage: claude-mem hook "),console.error("Platforms: claude-code, cursor, raw"),console.error("Events: context, session-init, observation, summarize, session-complete"),process.exit(1));let a=await Wa(r),c=!1;if(!a)try{w.info("SYSTEM","Starting worker in-process for hook",{event:o}),await new yp().start(),c=!0}catch(l){w.failure("SYSTEM","Worker failed to start in hook",{},l),Yn(),process.exit(0)}let{hookCommand:u}=await Promise.resolve().then(()=>(lF(),uF));await u(s,o,{skipExit:c});break}case"generate":{let i=process.argv.includes("--dry-run"),{generateClaudeMd:s}=await Promise.resolve().then(()=>(Ik(),Tk)),o=await s(i);process.exit(o)}case"clean":{let i=process.argv.includes("--dry-run"),{cleanClaudeMd:s}=await Promise.resolve().then(()=>(Ik(),Tk)),o=await s(i);process.exit(o)}default:{let i=Um();i&&j0(i.pid)&&(w.info("SYSTEM","Worker already running (PID alive), refusing to start duplicate",{existingPid:i.pid,existingPort:i.port,startedAt:i.startedAt}),process.exit(0)),await Wa(r)&&(w.info("SYSTEM","Port already in use, refusing to start duplicate",{port:r}),process.exit(0)),process.on("unhandledRejection",o=>{w.error("SYSTEM","Unhandled rejection in daemon",{reason:o instanceof Error?o.message:String(o)})}),process.on("uncaughtException",o=>{w.error("SYSTEM","Uncaught exception in daemon",{},o)}),new yp().start().catch(o=>{w.failure("SYSTEM","Worker failed to start",{},o),Yn(),process.exit(0)})}}}var Hge=typeof require<"u"&&typeof module<"u"?require.main===module||!module.parent:Bge.url===`file://${process.argv[1]}`||process.argv[1]?.endsWith("worker-service");Hge&&Fge();0&&(module.exports={WorkerService,buildStatusOutput,isPluginDisabledInClaudeSettings}); + `).run(Date.now(),...l);p.changes>0&&w.info("SYSTEM",`Marked ${p.changes} pending messages from stale sessions as failed`)}}catch(u){w.error("SYSTEM","Failed to clean up stale sessions",{},u)}let a=n.getSessionsWithPendingMessages(),c={totalPendingSessions:a.length,sessionsStarted:0,sessionsSkipped:0,startedSessionIds:[]};if(a.length===0)return c;w.info("SYSTEM",`Processing up to ${e} of ${a.length} pending session queues`);for(let u of a){if(c.sessionsStarted>=e)break;try{if(this.sessionManager.getSession(u)?.generatorPromise){c.sessionsSkipped++;continue}let d=this.sessionManager.initializeSession(u);w.info("SYSTEM",`Starting processor for session ${u}`,{project:d.project,pendingCount:n.getPendingCount(u)}),this.startSessionProcessor(d,"startup-recovery"),c.sessionsStarted++,c.startedSessionIds.push(u),await new Promise(p=>setTimeout(p,100))}catch(l){w.error("SYSTEM",`Failed to process session ${u}`,{},l),c.sessionsSkipped++}}return c}async shutdown(){this.stopOrphanReaper&&(this.stopOrphanReaper(),this.stopOrphanReaper=null),this.staleSessionReaperInterval&&(clearInterval(this.staleSessionReaperInterval),this.staleSessionReaperInterval=null),await xA({server:this.server.getHttpServer(),sessionManager:this.sessionManager,mcpClient:this.mcpClient,dbManager:this.dbManager,chromaMcpManager:this.chromaMcpManager||void 0})}broadcastProcessingStatus(){let e=this.sessionManager.isAnySessionProcessing(),r=this.sessionManager.getTotalActiveWork(),n=this.sessionManager.getActiveSessionCount();w.info("WORKER","Broadcasting processing status",{isProcessing:e,queueDepth:r,activeSessions:n}),this.sseBroadcaster.broadcast({type:"processing_status",isProcessing:e,queueDepth:r})}};async function hF(t){if(hA(),await Ka(t,1e3)){let s=await SA(t);if(s.matches)return w.info("SYSTEM","Worker already running and healthy"),!0;if(fA(15e3)){if(w.info("SYSTEM","Version mismatch detected but PID file is recent \u2014 another restart likely in progress, polling health",{pluginVersion:s.pluginVersion,workerVersion:s.workerVersion}),await Ka(t,15e3))return w.info("SYSTEM","Worker became healthy after waiting for concurrent restart"),!0;w.warn("SYSTEM","Worker did not become healthy after waiting \u2014 proceeding with own restart")}if(w.info("SYSTEM","Worker version mismatch detected - auto-restarting",{pluginVersion:s.pluginVersion,workerVersion:s.workerVersion}),await qm(t),!await Lm(t,Os(Cr.PORT_IN_USE_WAIT)))return w.error("SYSTEM","Port did not free up after shutdown for version mismatch restart",{port:t}),!1;Yn()}return await Wa(t)?(w.info("SYSTEM","Port in use, waiting for worker to become healthy"),await Ka(t,Os(Cr.PORT_IN_USE_WAIT))?(w.info("SYSTEM","Worker is now healthy"),!0):(w.error("SYSTEM","Port in use but worker not responding to health checks"),!1)):zge()?(w.warn("SYSTEM","Worker unavailable on Windows \u2014 skipping spawn (recent attempt failed within cooldown)"),!1):(w.info("SYSTEM","Starting worker daemon"),Uge(),D0(__filename,t)===void 0?(w.error("SYSTEM","Failed to spawn worker daemon"),!1):await Ka(t,Os(Cr.POST_SPAWN_WAIT))?(await bA(t,Os(Cr.READINESS_WAIT))||w.warn("SYSTEM","Worker is alive but readiness timed out \u2014 proceeding anyway"),Lge(),mA(),w.info("SYSTEM","Worker started successfully"),!0):(Yn(),w.error("SYSTEM","Worker failed to start (health check timeout)"),!1))}async function Fge(){let t=process.argv[2];(["start","hook","restart","--daemon"].includes(t)||t===void 0)&&jm()&&process.exit(0);let r=Et();function n(i,s){let o=gF(i,s);console.log(JSON.stringify(o)),process.exit(0)}switch(t){case"start":{await hF(r)?n("ready"):n("error","Failed to start worker");break}case"stop":{await qm(r),await Lm(r,Os(15e3))||w.warn("SYSTEM","Port did not free up after shutdown",{port:r}),Yn(),w.info("SYSTEM","Worker stopped successfully"),process.exit(0);break}case"restart":{w.info("SYSTEM","Restarting worker"),await qm(r),await Lm(r,Os(15e3))||(w.error("SYSTEM","Port did not free up after shutdown, aborting restart",{port:r}),process.exit(0)),Yn(),D0(__filename,r)===void 0&&(w.error("SYSTEM","Failed to spawn worker daemon during restart"),process.exit(0)),await Ka(r,Os(Cr.POST_SPAWN_WAIT))||(Yn(),w.error("SYSTEM","Worker failed to restart"),process.exit(0)),w.info("SYSTEM","Worker restarted successfully"),process.exit(0);break}case"status":{let i=await Wa(r),s=Um();i&&s?(console.log("Worker is running"),console.log(` PID: ${s.pid}`),console.log(` Port: ${s.port}`),console.log(` Started: ${s.startedAt}`)):console.log("Worker is not running"),process.exit(0);break}case"cursor":{let i=process.argv[3],s=await Z2(i,process.argv.slice(4));process.exit(s);break}case"hook":{await hF(r)||w.warn("SYSTEM","Worker failed to start before hook, handler will retry");let s=process.argv[3],o=process.argv[4];(!s||!o)&&(console.error("Usage: claude-mem hook "),console.error("Platforms: claude-code, cursor, raw"),console.error("Events: context, session-init, observation, summarize, session-complete"),process.exit(1));let a=await Wa(r),c=!1;if(!a)try{w.info("SYSTEM","Starting worker in-process for hook",{event:o}),await new yp().start(),c=!0}catch(l){w.failure("SYSTEM","Worker failed to start in hook",{},l),Yn(),process.exit(0)}let{hookCommand:u}=await Promise.resolve().then(()=>(lF(),uF));await u(s,o,{skipExit:c});break}case"generate":{let i=process.argv.includes("--dry-run"),{generateClaudeMd:s}=await Promise.resolve().then(()=>(Ik(),Tk)),o=await s(i);process.exit(o);break}case"clean":{let i=process.argv.includes("--dry-run"),{cleanClaudeMd:s}=await Promise.resolve().then(()=>(Ik(),Tk)),o=await s(i);process.exit(o);break}default:{let i=Um();i&&j0(i.pid)&&(w.info("SYSTEM","Worker already running (PID alive), refusing to start duplicate",{existingPid:i.pid,existingPort:i.port,startedAt:i.startedAt}),process.exit(0)),await Wa(r)&&(w.info("SYSTEM","Port already in use, refusing to start duplicate",{port:r}),process.exit(0)),process.on("unhandledRejection",o=>{w.error("SYSTEM","Unhandled rejection in daemon",{reason:o instanceof Error?o.message:String(o)})}),process.on("uncaughtException",o=>{w.error("SYSTEM","Uncaught exception in daemon",{},o)}),new yp().start().catch(o=>{w.failure("SYSTEM","Worker failed to start",{},o),Yn(),process.exit(0)})}}}var Hge=typeof require<"u"&&typeof module<"u"?require.main===module||!module.parent:Bge.url===`file://${process.argv[1]}`||process.argv[1]?.endsWith("worker-service");Hge&&Fge().catch(t=>{w.error("SYSTEM","Fatal error in main",{},t instanceof Error?t:void 0),process.exit(0)});0&&(module.exports={WorkerService,buildStatusOutput,isPluginDisabledInClaudeSettings}); /*! Bundled license information: depd/index.js: diff --git a/src/services/worker-service.ts b/src/services/worker-service.ts index 0bdf0be3..02dd4f32 100644 --- a/src/services/worker-service.ts +++ b/src/services/worker-service.ts @@ -1027,6 +1027,7 @@ async function main() { } else { exitWithStatus('error', 'Failed to start worker'); } + break; } case 'stop': { @@ -1038,6 +1039,7 @@ async function main() { removePidFile(); logger.info('SYSTEM', 'Worker stopped successfully'); process.exit(0); + break; } case 'restart': { @@ -1074,6 +1076,7 @@ async function main() { logger.info('SYSTEM', 'Worker restarted successfully'); process.exit(0); + break; } case 'status': { @@ -1088,12 +1091,14 @@ async function main() { console.log('Worker is not running'); } process.exit(0); + break; } case 'cursor': { const subcommand = process.argv[3]; const cursorResult = await handleCursorCommand(subcommand, process.argv.slice(4)); process.exit(cursorResult); + break; } case 'hook': { @@ -1147,6 +1152,7 @@ async function main() { const { generateClaudeMd } = await import('../cli/claude-md-commands.js'); const result = await generateClaudeMd(dryRun); process.exit(result); + break; } case 'clean': { @@ -1154,6 +1160,7 @@ async function main() { const { cleanClaudeMd } = await import('../cli/claude-md-commands.js'); const result = await cleanClaudeMd(dryRun); process.exit(result); + break; } case '--daemon': @@ -1210,5 +1217,8 @@ const isMainModule = typeof require !== 'undefined' && typeof module !== 'undefi : import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith('worker-service'); if (isMainModule) { - main(); + main().catch((error) => { + logger.error('SYSTEM', 'Fatal error in main', {}, error instanceof Error ? error : undefined); + process.exit(0); // Exit 0: don't block Claude Code, don't leave Windows Terminal tabs open + }); } diff --git a/tests/infrastructure/plugin-distribution.test.ts b/tests/infrastructure/plugin-distribution.test.ts index 45d37993..0bd58d6b 100644 --- a/tests/infrastructure/plugin-distribution.test.ts +++ b/tests/infrastructure/plugin-distribution.test.ts @@ -81,6 +81,22 @@ describe('Plugin Distribution - hooks.json Integrity', () => { } } }); + + it('should include CLAUDE_PLUGIN_ROOT fallback in all hook commands (#1215)', () => { + const hooksPath = path.join(projectRoot, 'plugin/hooks/hooks.json'); + const parsed = JSON.parse(readFileSync(hooksPath, 'utf-8')); + const expectedFallbackPath = '$HOME/.claude/plugins/marketplaces/thedotmack/plugin'; + + for (const [eventName, matchers] of Object.entries(parsed.hooks)) { + for (const matcher of matchers as any[]) { + for (const hook of matcher.hooks) { + if (hook.type === 'command') { + expect(hook.command).toContain(expectedFallbackPath); + } + } + } + } + }); }); describe('Plugin Distribution - package.json Files Field', () => {