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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmHCQjJHUyWwxGAVVjNLgp
This commit is contained in:
parent
18b0df9a24
commit
7d53a7325e
8 changed files with 22 additions and 6 deletions
12
CHANGELOG.md
12
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
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,4 +93,4 @@ if (recent.length >= TIER2_SESSION_THRESHOLD) {
|
|||
}
|
||||
}
|
||||
|
||||
outputWithContext(msg);
|
||||
outputWithContext(msg, 'SessionStart');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue