From 7d53a7325e85c1fe12d2124b93137f016cb0aefa Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Wed, 24 Jun 2026 06:46:11 +0200 Subject: [PATCH] fix(hooks): include required hookEventName in hookSpecificOutput JSON outputWithContext() emitted a hookSpecificOutput object without the required hookEventName field, so Claude Code rejected the hook output ("hookSpecificOutput is missing required field hookEventName") and dropped the context the SessionStart, UserPromptSubmit, and PostToolUse hooks tried to inject. Thread the event name through the helper; each caller now passes its own (SessionStart / UserPromptSubmit / PostToolUse). Existing emission tests now assert hookEventName per event. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01VmHCQjJHUyWwxGAVVjNLgp --- CHANGELOG.md | 12 ++++++++++++ hooks/scripts/lib.mjs | 3 ++- hooks/scripts/prompt-analyzer.mjs | 2 +- hooks/scripts/session-start.mjs | 2 +- hooks/scripts/tool-tracker.mjs | 6 +++--- tests/prompt-analyzer.test.mjs | 1 + tests/session-start.test.mjs | 1 + tests/tool-tracker.test.mjs | 1 + 8 files changed, 22 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee5d781..68e1773 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to this project will be documented in this file. +## [Unreleased] + +### Fixed + +- **Hooks emitted invalid JSON, so Claude Code dropped their injected + context.** `outputWithContext()` wrote a `hookSpecificOutput` object + without the required `hookEventName` field; the harness rejected it + ("hookSpecificOutput is missing required field hookEventName") and + discarded the context the SessionStart, UserPromptSubmit, and PostToolUse + hooks tried to inject. The helper now takes the event name and every + caller passes its own (`SessionStart` / `UserPromptSubmit` / `PostToolUse`). + ## [1.2.0] — 2026-05-01 Research-paper-driven detector update. Implements operational findings from diff --git a/hooks/scripts/lib.mjs b/hooks/scripts/lib.mjs index a123590..1ddbdba 100644 --- a/hooks/scripts/lib.mjs +++ b/hooks/scripts/lib.mjs @@ -289,10 +289,11 @@ export function outputContinue() { process.stdout.write(JSON.stringify({ continue: true }) + '\n'); } -export function outputWithContext(message) { +export function outputWithContext(message, hookEventName) { process.stdout.write(JSON.stringify({ continue: true, hookSpecificOutput: { + hookEventName, additionalContext: message } }) + '\n'); diff --git a/hooks/scripts/prompt-analyzer.mjs b/hooks/scripts/prompt-analyzer.mjs index da39fd6..2ad40a3 100644 --- a/hooks/scripts/prompt-analyzer.mjs +++ b/hooks/scripts/prompt-analyzer.mjs @@ -487,7 +487,7 @@ if (warnings.length > 0) { const freshState = readState(); freshState.last_warning_epoch = nowEpoch(); writeState(freshState); - outputWithContext(warnings.join(' ')); + outputWithContext(warnings.join(' '), 'UserPromptSubmit'); } else { outputContinue(); } diff --git a/hooks/scripts/session-start.mjs b/hooks/scripts/session-start.mjs index 99f4335..fd98297 100644 --- a/hooks/scripts/session-start.mjs +++ b/hooks/scripts/session-start.mjs @@ -93,4 +93,4 @@ if (recent.length >= TIER2_SESSION_THRESHOLD) { } } -outputWithContext(msg); +outputWithContext(msg, 'SessionStart'); diff --git a/hooks/scripts/tool-tracker.mjs b/hooks/scripts/tool-tracker.mjs index 7d67854..6b1a9ac 100644 --- a/hooks/scripts/tool-tracker.mjs +++ b/hooks/scripts/tool-tracker.mjs @@ -127,7 +127,7 @@ const late = isLateNight() ? ' Late-night session.' : ''; // No warnings — just periodic reminder at modulo-25 if (!level) { if (toolCount % 25 === 0) { - outputWithContext('REMINDER (Interaction Awareness): Check your next response against these rules — no unearned affirmations, no reformulating the user\'s words in stronger terms, no skipping counterarguments to stay agreeable. If you detect a reinforcement loop, scope escalation, or narrative crystallization: name it now.'); + outputWithContext('REMINDER (Interaction Awareness): Check your next response against these rules — no unearned affirmations, no reformulating the user\'s words in stronger terms, no skipping counterarguments to stay agreeable. If you detect a reinforcement loop, scope escalation, or narrative crystallization: name it now.', 'PostToolUse'); } else { outputContinue(); } @@ -141,7 +141,7 @@ const elapsed = nowTs - lastWarning; if (lastWarning > 0 && elapsed < cooldown) { // Still in cooldown — send periodic reminder instead if at modulo-25 if (toolCount % 25 === 0) { - outputWithContext('REMINDER (Interaction Awareness): Check your next response against these rules — no unearned affirmations, no reformulating the user\'s words in stronger terms, no skipping counterarguments to stay agreeable.'); + outputWithContext('REMINDER (Interaction Awareness): Check your next response against these rules — no unearned affirmations, no reformulating the user\'s words in stronger terms, no skipping counterarguments to stay agreeable.', 'PostToolUse'); } else { outputContinue(); } @@ -163,4 +163,4 @@ state = readState(); state.last_warning_epoch = nowTs; writeState(state); -outputWithContext(warning); +outputWithContext(warning, 'PostToolUse'); diff --git a/tests/prompt-analyzer.test.mjs b/tests/prompt-analyzer.test.mjs index d68c5e2..0508423 100644 --- a/tests/prompt-analyzer.test.mjs +++ b/tests/prompt-analyzer.test.mjs @@ -273,6 +273,7 @@ describe('thresholds and cooldowns', () => { dir = setupTestDir(); createStateFile(dir, 'p1', { ...freshState(), dep_flags: 1 }); const out = runHook('prompt-analyzer.mjs', { session_id: 'p1', prompt: 'tell me what to do' }, dir); + assert.equal(out.hookSpecificOutput?.hookEventName, 'UserPromptSubmit'); assert.ok(out.hookSpecificOutput?.additionalContext?.includes('Dependency language noticed')); }); diff --git a/tests/session-start.test.mjs b/tests/session-start.test.mjs index a4efa57..bb8af98 100644 --- a/tests/session-start.test.mjs +++ b/tests/session-start.test.mjs @@ -12,6 +12,7 @@ describe('session-start', () => { dir = setupTestDir(); const out = runHook('session-start.mjs', { session_id: 's1', cwd: '/tmp' }, dir); assert.equal(out.continue, true); + assert.equal(out.hookSpecificOutput.hookEventName, 'SessionStart'); assert.ok(out.hookSpecificOutput.additionalContext.includes('Interaction Awareness is active')); const state = readState(dir, 's1'); assert.ok(state); diff --git a/tests/tool-tracker.test.mjs b/tests/tool-tracker.test.mjs index ad20e3a..b1ab681 100644 --- a/tests/tool-tracker.test.mjs +++ b/tests/tool-tracker.test.mjs @@ -74,6 +74,7 @@ describe('tool-tracker', () => { dir = setupTestDir(); createStateFile(dir, 't6', freshState({ tool_count: 24 })); const out = runHook('tool-tracker.mjs', { session_id: 't6', tool_name: 'Read' }, dir); + assert.equal(out.hookSpecificOutput?.hookEventName, 'PostToolUse'); assert.ok(out.hookSpecificOutput?.additionalContext?.includes('REMINDER')); });