.maestro/playbooks/2026-02-23-Issue-Triage/2026-02-23-Root-Cause-Fixes/TRIAGE-04-Hook-Lifecycle-Fixes.md
The Stop hook creates an infinite loop because its summarize output gets interpreted as new instructions by Claude Code (#987). The fix is ensuring Stop/Summary hooks return suppressOutput: true and never emit stdout. Secondary: stderr from hooks shows as error UI in Claude Code (#1181). These are specific, targeted fixes — not a rate limiter framework.
Issues resolved: #987, #984, #975 (Stop hook loop), #1181 (stderr as errors), #598 (conversation pollution), #784 (agent output leaks)
Stop hook loop — The Stop hook generates summarization output. Claude Code interprets any stdout from a hook as instructions, creating a feedback loop. The fix is suppressOutput: true in the hook response, not a rate limiter.
stderr as UI errors — Claude Code displays stderr content to users as error messages. The logger or hook code writes to stderr for diagnostic purposes, which users see as errors. Fix: suppress stderr in hook context.
Fix the Stop hook infinite loop:
session-complete in src/cli/handlers/{ continue: true, suppressOutput: true } — no stdout contentsrc/cli/adapters/claude-code.ts or src/hooks/hook-response.ts) and ensure Stop/Summary hook types always set suppressOutput: truesession-complete as a recognized typesession-complete handler exists with suppressOutput: true, adapter defaults to suppressOutput: true, unknown event types return no-op with exit 0.Fix stderr showing as errors in Claude Code UI (#1181):
src/utils/logger.ts — if it writes to stderr, add a check: when running in hook context (detect via env var or process context), suppress stderr outputprocess.stderr.write = () => true or pipe to the log fileprocess.stderr.write = (() => true) at start of hookCommand() with finally block restore. Converted console.error() to logger.warn()/logger.error() in hook-command.ts and handlers/index.ts.Fix conversation history pollution (#598, #784):
src/cli/handlers/ — every handler that returns output should set suppressOutput: true unless it's specifically injecting context (like SessionStart context injection)--continue case (#784), the memory agent's internal processing output should never be visible — ensure the summarization flow uses suppressOutput: truesuppressOutput: true. Adapter defaults to suppressOutput: true. Context handler uses hookSpecificOutput (correct for context injection). stderr suppression from task 2 covers the remaining conversation pollution vector. Run npm test and fix any failures
tests/hook-lifecycle.test.ts, all passing. 52 total hook-related tests pass. Full suite: 954 pass, 21 fail (all pre-existing baseline failures, none from TRIAGE-04 changes).