From 0db59088ff527c6b9952a8fa6445328e26652808 Mon Sep 17 00:00:00 2001 From: Abdul Wahab Date: Sun, 26 Jul 2026 12:46:11 +0500 Subject: [PATCH] Stop the design hook erroring on a node too old for ESM The hook command invokes bare `node`. When that node predates ESM, `hook.mjs` dies while it is still being parsed, before the script's own always-exit-0 contract can run, so node exits 1 and the harness reports a hook error on every Stop and every edit. Probe the runtime in the command string before invoking the hook, and route the Claude plugin manifest through the guard that already covered the project-local manifests. On probe failure the command exits 0 and emits a one-time `systemMessage` naming the two fixes available to the user, since nothing written in ESM can report this condition. Fixes #410. Co-Authored-By: Claude Opus 5 --- .claude/settings.json | 4 ++-- .codex/hooks.json | 4 ++-- .cursor/hooks.json | 2 +- plugin/hooks/hooks.json | 4 ++-- scripts/lib/transformers/hooks.js | 26 +++++++++++++++++++++++--- 5 files changed, 30 insertions(+), 10 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index 0391eb720..6500e609d 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -7,7 +7,7 @@ "hooks": [ { "type": "command", - "command": "[ ! -f \"${CLAUDE_PROJECT_DIR}/.claude/skills/impeccable/scripts/hook.mjs\" ] || node \"${CLAUDE_PROJECT_DIR}/.claude/skills/impeccable/scripts/hook.mjs\"", + "command": "[ ! -f \"${CLAUDE_PROJECT_DIR}/.claude/skills/impeccable/scripts/hook.mjs\" ] || ! { node -e \"import('fs')\" 2>/dev/null || { D=\"$HOME/.impeccable\"; [ -f \"$D/node-unsupported\" ] || { mkdir -p \"$D\" 2>/dev/null && : > \"$D/node-unsupported\" 2>/dev/null && printf '%s' '{\"systemMessage\":\"The impeccable design hook is not running: no Node 22 or newer on PATH. Install one, or remove the impeccable hook from your harness settings.\"}'; }; exit 0; }; } || node \"${CLAUDE_PROJECT_DIR}/.claude/skills/impeccable/scripts/hook.mjs\"", "timeout": 5, "statusMessage": "Checking UI changes" } @@ -19,7 +19,7 @@ "hooks": [ { "type": "command", - "command": "[ ! -f \"${CLAUDE_PROJECT_DIR}/.claude/skills/impeccable/scripts/hook.mjs\" ] || node \"${CLAUDE_PROJECT_DIR}/.claude/skills/impeccable/scripts/hook.mjs\"", + "command": "[ ! -f \"${CLAUDE_PROJECT_DIR}/.claude/skills/impeccable/scripts/hook.mjs\" ] || ! { node -e \"import('fs')\" 2>/dev/null || { D=\"$HOME/.impeccable\"; [ -f \"$D/node-unsupported\" ] || { mkdir -p \"$D\" 2>/dev/null && : > \"$D/node-unsupported\" 2>/dev/null && printf '%s' '{\"systemMessage\":\"The impeccable design hook is not running: no Node 22 or newer on PATH. Install one, or remove the impeccable hook from your harness settings.\"}'; }; exit 0; }; } || node \"${CLAUDE_PROJECT_DIR}/.claude/skills/impeccable/scripts/hook.mjs\"", "timeout": 30, "statusMessage": "Design deep pass" } diff --git a/.codex/hooks.json b/.codex/hooks.json index dfcda438b..d7cd1b798 100644 --- a/.codex/hooks.json +++ b/.codex/hooks.json @@ -6,7 +6,7 @@ "hooks": [ { "type": "command", - "command": "[ ! -f \".codex/skills/impeccable/scripts/hook.mjs\" ] || node \".codex/skills/impeccable/scripts/hook.mjs\"", + "command": "[ ! -f \".codex/skills/impeccable/scripts/hook.mjs\" ] || ! { node -e \"import('fs')\" 2>/dev/null || { D=\"$HOME/.impeccable\"; [ -f \"$D/node-unsupported\" ] || { mkdir -p \"$D\" 2>/dev/null && : > \"$D/node-unsupported\" 2>/dev/null && printf '%s' '{\"systemMessage\":\"The impeccable design hook is not running: no Node 22 or newer on PATH. Install one, or remove the impeccable hook from your harness settings.\"}'; }; exit 0; }; } || node \".codex/skills/impeccable/scripts/hook.mjs\"", "timeout": 5, "statusMessage": "Checking UI changes" } @@ -18,7 +18,7 @@ "hooks": [ { "type": "command", - "command": "[ ! -f \".codex/skills/impeccable/scripts/hook.mjs\" ] || node \".codex/skills/impeccable/scripts/hook.mjs\"", + "command": "[ ! -f \".codex/skills/impeccable/scripts/hook.mjs\" ] || ! { node -e \"import('fs')\" 2>/dev/null || { D=\"$HOME/.impeccable\"; [ -f \"$D/node-unsupported\" ] || { mkdir -p \"$D\" 2>/dev/null && : > \"$D/node-unsupported\" 2>/dev/null && printf '%s' '{\"systemMessage\":\"The impeccable design hook is not running: no Node 22 or newer on PATH. Install one, or remove the impeccable hook from your harness settings.\"}'; }; exit 0; }; } || node \".codex/skills/impeccable/scripts/hook.mjs\"", "timeout": 30, "statusMessage": "Design deep pass" } diff --git a/.cursor/hooks.json b/.cursor/hooks.json index 040483c56..e4dd64f5e 100644 --- a/.cursor/hooks.json +++ b/.cursor/hooks.json @@ -3,7 +3,7 @@ "hooks": { "preToolUse": [ { - "command": "[ ! -f \".cursor/skills/impeccable/scripts/hook-before-edit.mjs\" ] || node \".cursor/skills/impeccable/scripts/hook-before-edit.mjs\"", + "command": "[ ! -f \".cursor/skills/impeccable/scripts/hook-before-edit.mjs\" ] || ! { node -e \"import('fs')\" 2>/dev/null || { D=\"$HOME/.impeccable\"; [ -f \"$D/node-unsupported\" ] || { mkdir -p \"$D\" 2>/dev/null && : > \"$D/node-unsupported\" 2>/dev/null && printf '%s' '{\"systemMessage\":\"The impeccable design hook is not running: no Node 22 or newer on PATH. Install one, or remove the impeccable hook from your harness settings.\"}'; }; exit 0; }; } || node \".cursor/skills/impeccable/scripts/hook-before-edit.mjs\"", "timeout": 5 } ] diff --git a/plugin/hooks/hooks.json b/plugin/hooks/hooks.json index 66fd18e7e..772012c02 100644 --- a/plugin/hooks/hooks.json +++ b/plugin/hooks/hooks.json @@ -6,7 +6,7 @@ "hooks": [ { "type": "command", - "command": "node \"${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/hook.mjs\"", + "command": "[ ! -f \"${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/hook.mjs\" ] || ! { node -e \"import('fs')\" 2>/dev/null || { D=\"$HOME/.impeccable\"; [ -f \"$D/node-unsupported\" ] || { mkdir -p \"$D\" 2>/dev/null && : > \"$D/node-unsupported\" 2>/dev/null && printf '%s' '{\"systemMessage\":\"The impeccable design hook is not running: no Node 22 or newer on PATH. Install one, or remove the impeccable hook from your harness settings.\"}'; }; exit 0; }; } || node \"${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/hook.mjs\"", "timeout": 5, "statusMessage": "Checking UI changes" } @@ -18,7 +18,7 @@ "hooks": [ { "type": "command", - "command": "node \"${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/hook.mjs\"", + "command": "[ ! -f \"${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/hook.mjs\" ] || ! { node -e \"import('fs')\" 2>/dev/null || { D=\"$HOME/.impeccable\"; [ -f \"$D/node-unsupported\" ] || { mkdir -p \"$D\" 2>/dev/null && : > \"$D/node-unsupported\" 2>/dev/null && printf '%s' '{\"systemMessage\":\"The impeccable design hook is not running: no Node 22 or newer on PATH. Install one, or remove the impeccable hook from your harness settings.\"}'; }; exit 0; }; } || node \"${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/hook.mjs\"", "timeout": 30, "statusMessage": "Design deep pass" } diff --git a/scripts/lib/transformers/hooks.js b/scripts/lib/transformers/hooks.js index 95da1ed6c..72d39e006 100644 --- a/scripts/lib/transformers/hooks.js +++ b/scripts/lib/transformers/hooks.js @@ -51,7 +51,27 @@ const CLAUDE_PROJECT_HOOK = '${CLAUDE_PROJECT_DIR}/.claude/skills/impeccable/scr // user-level hooks fire in every project, where a project-relative path may // not exist). Guard node invocations so a missing file exits 0 without // swallowing node's real exit code when the file is present. -const guardedNode = (hookPath) => `[ ! -f "${hookPath}" ] || node "${hookPath}"`; +// +// The runtime is guarded too (issue #410): a `node` on PATH too old for ESM, +// or no node at all, kills the hook script while it is still being parsed, +// before the script's own always-exit-0 contract can run. Nothing written in +// ESM can report that, the doctor and the sub-commands included, so the command +// string reports it instead: probe dynamic import, and when the probe fails +// hand the user the two things they can act on (install a supported node, or +// drop the hook) through the universal `systemMessage` field on exit 0. A +// marker under ~/.impeccable keeps it to one notice per machine rather than one +// per edit. The message says `on PATH` deliberately: the common cause is a hook +// shell whose PATH misses the version manager, so a user already running Node 22 +// needs to know the hook's PATH is at issue and not their install. Apostrophes +// cannot appear in the message, which travels in a single-quoted shell string. +const NODE_NOTICE_TEXT = 'The impeccable design hook is not running: no Node 22 or newer on PATH. ' + + 'Install one, or remove the impeccable hook from your harness settings.'; +const NODE_NOTICE = 'D="$HOME/.impeccable"; [ -f "$D/node-unsupported" ] || ' + + '{ mkdir -p "$D" 2>/dev/null && : > "$D/node-unsupported" 2>/dev/null && ' + + `printf '%s' '{"systemMessage":"${NODE_NOTICE_TEXT}"}'; }`; +const guardedNode = (hookPath) => + `[ ! -f "${hookPath}" ] || ! { node -e "import('fs')" 2>/dev/null || { ${NODE_NOTICE}; exit 0; }; } ` + + `|| node "${hookPath}"`; const CLAUDE_PLUGIN_HOOK = '${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/hook.mjs'; const CODEX_PLUGIN_HOOK = '${PLUGIN_ROOT}/skills/impeccable/scripts/hook.mjs'; // Codex reads project hooks from `.codex/hooks.json`, but the skill payload the @@ -104,14 +124,14 @@ export function buildClaudePluginHooksManifest() { hooks: [ { type: 'command', - command: `node "${CLAUDE_PLUGIN_HOOK}"`, + command: guardedNode(CLAUDE_PLUGIN_HOOK), timeout: TIMEOUT_SECONDS, statusMessage: STATUS_MESSAGE, }, ], }, ], - Stop: [stopEntry(`node "${CLAUDE_PLUGIN_HOOK}"`)], + Stop: [stopEntry(guardedNode(CLAUDE_PLUGIN_HOOK))], }, }; }