#3457 follow-up · playground · 2026-08-12

The two events #3457 left red, settled.

#3457 made derive-cc-output-keys.mjs --check bidirectional: an EVENTS_WITH_ADDITIONAL_CONTEXT entry the CC binary's prose doesn't corroborate now fails the nightly cc-contract-probe unless it carries a reviewed, trace-and-observe exception. PostCompact and PostToolUseFailure were left deliberately unsettled — the issue that shipped the gate is not the issue that should also rush its last two entries. This PR produces that evidence: PostToolUseFailure is supported, added as a fifth reviewed exception. PostCompact is not — removed from the allow-list entirely, and lifecycle/post-compact-recovery.ts no longer builds an envelope CC throws away.

Try it: the gate, before → after this PR

Before · #3457's exit=3
After · this PR's exit=0
CC 2.1.228 — events the binary NAMES as additionalContext consumers:
  PostToolBatch
  Stop
  SubagentStop

allow-list: 9 asserted / 3 corroborated by the binary / 4 reviewed exception / 2 unreviewed

CONFIRMED by trace-and-observe, not binary prose (reviewed exception):
  PostToolUse
  PreToolUse
  SessionStart
  UserPromptSubmit

DRIFT: no binary string names these events as additionalContext
consumers, and they carry no reviewed exception (#3418):
  PostCompact
  PostToolUseFailure

exit=3   ← the nightly probe, red since #3457 landed
CC 2.1.228 — events the binary NAMES as additionalContext consumers:
  PostToolBatch
  Stop
  SubagentStop

allow-list: 8 asserted / 3 corroborated by the binary / 5 reviewed exception / 0 unreviewed

CONFIRMED by trace-and-observe, not binary prose (reviewed exception):
  PostToolUse
  PostToolUseFailure
  PreToolUse
  SessionStart
  UserPromptSubmit

OK: every event the binary names is present in the generated allow-list,
and every uncorroborated entry carries a reviewed exception.
exit=0   ← legitimately green — one fewer asserted event, not a masked failure

Both panels are the script's actual stdout, captured 2026-08-12 running node scripts/derive-cc-output-keys.mjs --check against the real installed CC 2.1.228 binary — not a mockup. Note the allow-list shrank from 9 asserted to 8: green here means PostCompact was removed, not quietly excepted.

Why neither event could be traced the way the first four were

The four exceptions #3457 shipped were each settled by watching a real hook's additionalContext arrive in a live session. Neither event here permits that from inside an agent session: PostToolUseFailure fires on a genuine tool-execution exception or a user interrupt (is_interrupt), never on the ordinary non-zero exit / tool_use_error content an agent can produce on demand — confirmed empirically: a Bash command not found (exit 127), an Edit with a missing old_string, and a Bash grep no-match all completed with zero posttool/failure-handler entries in hook-timing.jsonl, while the same log shows 50 real firings elsewhere on this machine today. PostCompact fires only on a real compaction, which nothing in an agent's tool surface can trigger. Both were instead settled by tracing the shipped 2.1.228 binary's own executor code — a level below the doc-prose regex the gate already checks, and independently reproducible with strings.

The trace: one marker, two outcomes

strings -a -n 6 claude-2.1.228 | grep -n hook_additional_context surfaces a token CC's executor emits once per event's dedicated code block — present for every event already proven to deliver additionalContext (Stop, SubagentStop, PostToolBatch, PreToolUse, UserPromptSubmit, SessionStart, PostToolUse), and absent for a negative control that never supported it (PermissionRequest). The two events in question fall on opposite sides:

eventexecutor block contentshook_additional_contextverdict
PostToolUse (already proven, #3418) additionalContext · hook_blocking_error · updatedToolOutput · updatedMCPToolOutput presentsupported
PostToolUseFailure hook_blocking_error · hook_error_during_execution · dispatch/cancel/timeout messages — same structural shape as PostToolUse's own block presentsupported — new exception
PostCompact (all 9 occurrences) "PostCompact […] completed successfully" · "…] failed" · compact_summary — no error/cancel/dispatch vocabulary at all absent, every timeunsupported — confirms #3321
PostToolUseFailure reviewed exception

#3418 already fixed the field read (input.tool_errorinput.error) but left the event itself unsettled — an unobservable hook proves nothing about CC support either way. The executor-string trace above is the evidence; see spec/cc-output-keys.spec.yml's reviewed_exceptions for the full citation. No code change to failure-handler.ts was needed — it already emits correctly.

PostCompact removed, not excepted

#3321 found this in March; a later "Correction 1" comment on that issue pushed back, but its only cited evidence was this repo's own allow-list membership plus a guard unit test asserting the guard doesn't strip the key — both downstream of the exact allow-list this gate exists to verify, not independent proof CC delivers it. The executor trace settles it: gone from EVENTS_WITH_ADDITIONAL_CONTEXT, not merely left off ADDITIONAL_CONTEXT_REVIEWED_EXCEPTIONS.

Stopping the emission into the void

lifecycle/post-compact-recovery.ts was the one hook still building a PostCompact additionalContext envelope. It kept computing the same recovered-context text (branch, active files, tasks, decisions, compaction analytics) but now logs it at debug level instead of returning it as dead hookSpecificOutput — the side-effecting half of the hook (nudge-outcome tracking, M119 #1476) is untouched, since that never depended on additionalContext in the first place.

- return outputWithContext(context);
+ hookCtx.log('post-compact-recovery', context, 'debug');
+ return outputSilentSuccess();

Verification

src/hooks vitest (full suite)         7567 passed, 2 skipped, 314 files
lifecycle-output-protocol.test.ts     updated  (PostCompact moved: allow-listed → stripped, dedicated block added)
output-guard-cc-contract.test.ts      updated  (PostCompact strip case added, PostToolUseFailure stays proven)
post-compact-recovery.test.ts         updated  (asserts against ctx.log, not outputWithContext; asserts silent return)
typecheck (src/hooks)                 0 errors
npm run build (root)                  clean, plugins/ regenerated, no count drift
derive-cc-output-keys.mjs --check     exit=0  (was exit=3 — cc-contract-probe back to legitimately green)

Scope note. Tracing confirmed a separate, wider issue in passing: outputWithContext() and several sibling builders in lib/output.ts hardcode a literal hookEventName (e.g. 'PostToolUse') regardless of which event actually fires the hook. output-guard.mjs already documents why fixing that is out of scope here — a mismatched-label rule was tried and reverted after it broke 9 security tests, because outputDeny/outputAsk/outputDefer rely on the same hardcoding for PermissionRequest. Left untouched; tracked separately, not by this PR.