fix(hooks): emit suggest-compact via hookSpecificOutput stdout

The threshold and interval suggestions in suggest-compact.js are written
to stderr via log(). Per the Claude Code hooks guide, non-blocking
PreToolUse stderr (exit code 0) is only captured in the debug log — it
does not reach the model. As shipped, the script's nudge to /compact is
silent on Claude Code 2.1.x.

Fix: alongside the existing log() call (kept for debug-log capture),
emit the same suggestion as structured JSON on stdout:

  { hookSpecificOutput: {
      hookEventName: "PreToolUse",
      additionalContext: msg
  } }

This is the documented mechanism for a PreToolUse hook to inject
context into the next model turn without blocking the tool call.

Verified end-to-end on Claude Code 2.1.142 (VSCode native extension,
Windows 11) — the additionalContext now surfaces in the next turn as
a <system-reminder> block. Counter increment and exit code behavior
unchanged.

Tests: 4 new cases in tests/hooks/suggest-compact.test.js covering
stdout JSON at threshold, stdout JSON at +25 interval, silence below
threshold, and stderr-retention for the debug log. Suite goes from
19/19 -> 23/23 (suggest-compact) and full run-all stays clean for
the unaffected suites (the 4 pre-existing Windows broken-symlink
failures in ci/validators, lib/session-manager, and lib/utils are
unrelated to this change).
This commit is contained in:
richm-spp
2026-05-15 09:48:28 +10:00
parent 0e66c838c7
commit c802c33abc
2 changed files with 76 additions and 4 deletions

View File

@@ -19,7 +19,8 @@ const {
getTempDir,
writeFile,
readStdinJson,
log
log,
output
} = require('../lib/utils');
async function resolveSessionId() {
@@ -77,14 +78,25 @@ async function main() {
writeFile(counterFile, String(count));
}
// Suggest compact after threshold tool calls
// Suggest compact after threshold tool calls.
//
// log() writes to stderr (debug log). Per the Claude Code hooks guide,
// non-blocking PreToolUse stderr (exit 0) is only written to the debug log;
// it does not reach the model. To inject a user-facing suggestion without
// blocking the tool call, emit structured JSON to stdout with
// hookSpecificOutput.additionalContext — the documented mechanism for
// PreToolUse hooks to add context to the next model turn.
if (count === threshold) {
log(`[StrategicCompact] ${threshold} tool calls reached - consider /compact if transitioning phases`);
const msg = `[StrategicCompact] ${threshold} tool calls reached - consider /compact if transitioning phases`;
log(msg);
output({ hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: msg } });
}
// Suggest at regular intervals after threshold (every 25 calls from threshold)
if (count > threshold && (count - threshold) % 25 === 0) {
log(`[StrategicCompact] ${count} tool calls - good checkpoint for /compact if context is stale`);
const msg = `[StrategicCompact] ${count} tool calls - good checkpoint for /compact if context is stale`;
log(msg);
output({ hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: msg } });
}
process.exit(0);