#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.
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.
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.
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:
| event | executor block contents | hook_additional_context | verdict |
|---|---|---|---|
PostToolUse (already proven, #3418) |
additionalContext · hook_blocking_error · updatedToolOutput · updatedMCPToolOutput |
present | supported |
PostToolUseFailure |
hook_blocking_error · hook_error_during_execution · dispatch/cancel/timeout messages
— same structural shape as PostToolUse's own block |
present | supported — new exception |
PostCompact (all 9 occurrences) |
"PostCompact […] completed successfully" · "…] failed" · compact_summary
— no error/cancel/dispatch vocabulary at all |
absent, every time | unsupported — confirms #3321 |
#3418 already fixed the field read
(input.tool_error → input.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.
#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.
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();
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.