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')); });