From 4f999ceff84d1d13d12a6778b3ab2330667019a3 Mon Sep 17 00:00:00 2001 From: Abdul Wahab Date: Sun, 26 Jul 2026 21:10:41 +0500 Subject: [PATCH] Give Codex the notice too; its hook reference documents systemMessage Commit 8397d532 took a reviewer's word that Codex expects hookSpecificOutput and dropped its notice on that basis. Codex documents `systemMessage` for PostToolUse and Stop as text shown as a warning in the UI or event stream, the same field Claude Code reads, so the notice belongs there and the earlier comment asserted something unverified. Checked the rest against their own references while here. Cursor's preToolUse output is permission-shaped and its user_message renders only when the action is DENIED, so warning would mean blocking the edit. Grok treats PostToolUse and Stop as passive events and ignores stdout outright. Copilot's contract is unconfirmed. Those three keep the probe alone, which is a verified limit now rather than an assumption. Co-Authored-By: Claude Opus 5 --- .codex/hooks.json | 4 +-- scripts/lib/transformers/hooks.js | 49 +++++++++++++++++++------------ 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/.codex/hooks.json b/.codex/hooks.json index b6784765e..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 -e \"import('fs')\" 2>/dev/null || 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 -e \"import('fs')\" 2>/dev/null || 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/scripts/lib/transformers/hooks.js b/scripts/lib/transformers/hooks.js index 443ab2ef3..f4745f125 100644 --- a/scripts/lib/transformers/hooks.js +++ b/scripts/lib/transformers/hooks.js @@ -59,13 +59,23 @@ const CLAUDE_PROJECT_HOOK = '${CLAUDE_PROJECT_DIR}/.claude/skills/impeccable/scr // string carries the probe: dynamic import, and no-op at exit 0 when it fails. // // `notice` is the shell that reports the dead runtime to the user, and it is -// passed in rather than baked in because the wire format is per harness. Claude -// Code reads a `systemMessage` field off stdout on exit 0. Codex expects -// `hookSpecificOutput` and Cursor a permission-shaped payload, so handing either -// a Claude response gets it discarded or printed raw; until those shapes are -// confirmed every non-Claude harness takes the probe alone, and an unsupported -// runtime stays as quiet there as it was before the probe existed. Adding a -// shape later is one more argument at that harness's call site. +// passed in rather than baked in because only some harnesses have a channel for +// it. Checked against each harness's hook reference, on the events we hook: +// +// Claude Code PostToolUse + Stop: `systemMessage` is a universal field shown +// to the user, parsed on exit 0. -> notice +// Codex PostToolUse + Stop: `systemMessage` is documented as text shown +// as a warning in the UI or event stream. -> notice +// Cursor preToolUse: output is permission-shaped, and its `user_message` +// is shown only when the action is DENIED. Warning would mean +// blocking the edit, which is worse than silence. -> probe only +// Grok Build PostToolUse + Stop are passive events: stdout is ignored +// outright, so a notice cannot reach anyone. -> probe only +// Copilot postToolUse: output contract not confirmed. Silence is the +// conservative read; do not guess a shape. -> probe only +// +// A harness with no channel still gets the probe, so an unsupported runtime stays +// as quiet there as it was before the probe existed. const guardedNode = (hookPath, notice = '') => { const probe = notice ? `! { node -e "import('fs')" 2>/dev/null || { ${notice}; exit 0; }; }` @@ -77,11 +87,14 @@ const guardedNode = (hookPath, notice = '') => { // the hook's PATH is at issue and not their install. Apostrophes cannot appear in // it, since it travels inside a single-quoted shell string. The marker under // ~/.impeccable holds it to one notice per machine rather than one per edit. -const CLAUDE_NODE_NOTICE_TEXT = 'The impeccable design hook is not running: no Node 22 or newer on PATH. ' +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 CLAUDE_NODE_NOTICE = 'D="$HOME/.impeccable"; [ -f "$D/node-unsupported" ] || ' +// Claude Code and Codex both read `systemMessage`, so one payload serves both. +// The marker is per machine, not per harness: a machine running both should be +// told once, not once each. +const SYSTEM_MESSAGE_NOTICE = 'D="$HOME/.impeccable"; [ -f "$D/node-unsupported" ] || ' + '{ mkdir -p "$D" 2>/dev/null && : > "$D/node-unsupported" 2>/dev/null && ' - + `printf '%s' '{"systemMessage":"${CLAUDE_NODE_NOTICE_TEXT}"}'; }`; + + `printf '%s' '{"systemMessage":"${NODE_NOTICE_TEXT}"}'; }`; 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 @@ -107,14 +120,14 @@ export function buildClaudeSettingsManifest() { hooks: [ { type: 'command', - command: guardedNode(CLAUDE_PROJECT_HOOK, CLAUDE_NODE_NOTICE), + command: guardedNode(CLAUDE_PROJECT_HOOK, SYSTEM_MESSAGE_NOTICE), timeout: TIMEOUT_SECONDS, statusMessage: STATUS_MESSAGE, }, ], }, ], - Stop: [stopEntry(guardedNode(CLAUDE_PROJECT_HOOK, CLAUDE_NODE_NOTICE))], + Stop: [stopEntry(guardedNode(CLAUDE_PROJECT_HOOK, SYSTEM_MESSAGE_NOTICE))], }, }; } @@ -134,14 +147,14 @@ export function buildClaudePluginHooksManifest() { hooks: [ { type: 'command', - command: guardedNode(CLAUDE_PLUGIN_HOOK, CLAUDE_NODE_NOTICE), + command: guardedNode(CLAUDE_PLUGIN_HOOK, SYSTEM_MESSAGE_NOTICE), timeout: TIMEOUT_SECONDS, statusMessage: STATUS_MESSAGE, }, ], }, ], - Stop: [stopEntry(guardedNode(CLAUDE_PLUGIN_HOOK, CLAUDE_NODE_NOTICE))], + Stop: [stopEntry(guardedNode(CLAUDE_PLUGIN_HOOK, SYSTEM_MESSAGE_NOTICE))], }, }; } @@ -158,14 +171,14 @@ export function buildCodexPluginHooksManifest() { hooks: [ { type: 'command', - command: guardedNode(CODEX_PLUGIN_HOOK), + command: guardedNode(CODEX_PLUGIN_HOOK, SYSTEM_MESSAGE_NOTICE), timeout: TIMEOUT_SECONDS, statusMessage: STATUS_MESSAGE, }, ], }, ], - Stop: [stopEntry(guardedNode(CODEX_PLUGIN_HOOK))], + Stop: [stopEntry(guardedNode(CODEX_PLUGIN_HOOK, SYSTEM_MESSAGE_NOTICE))], }, }; } @@ -183,14 +196,14 @@ export function buildCodexHooksManifest(skillDir = '.codex') { hooks: [ { type: 'command', - command: guardedNode(hookPath), + command: guardedNode(hookPath, SYSTEM_MESSAGE_NOTICE), timeout: TIMEOUT_SECONDS, statusMessage: STATUS_MESSAGE, }, ], }, ], - Stop: [stopEntry(guardedNode(hookPath))], + Stop: [stopEntry(guardedNode(hookPath, SYSTEM_MESSAGE_NOTICE))], }, }; }