#3335 A1-A6 · #3321 claims 5+6 · playground · 2026-08-11

Eight hooks were listening on the wrong frequency.

The deprecation-direction sweep extracted every payload builder from the shipped 2.1.227 binary and diffed them against what ork's hooks read. Six live handlers read fields no CC version has ever sent. Their unit tests stayed green the whole time, because the fixtures were hand-built from the same wrong type declarations: a closed loop that never touched a real payload.

What CC sends vs what ork read

event, payload verbatim from the binaryork readeffect
Elicitation {mcp_server_name, requested_schema, ...}server_name, form_schemasecret-field block could not fire
FileChanged {file_path, event}changed_file_pathhook inert for all 4 matchers
TeammateIdle {teammate_name, team_name}teammate_type, idle_duration_mstelemetry logged fabricated 0s as measured
PostCompact {trigger, compact_summary}compaction_count, context_size_aftersilent fallbacks
StopFailure {error, error_details, ...}stop_failure_reason, api_status_codereason=unknown

The guard that was born dead twice

elicitation-guard, and the comment that inverted reality

The original hook read invented field names. #1264 Phase 3 "fixed" it onto server_name/form_schema and left a comment in types.ts asserting mcp_server_name was the invented name. The binary says the opposite: form_schema appears 0 times, requested_schema 7, mcp_server_name 30. So the fix for the born-dead guard made it born-dead again, with a comment warning future maintainers away from the correct answer. The new comment records both inversions and says: extract the payload builder from the binary before "correcting" this a third time.

The blocks that fired with empty messages (#3321 claim 6)

CC 2.1.227, decision:'block' command-hook branch:
  output = json.reason || stderr || ""      stopReason never consulted here

both PreCompact blockers emitted:  {continue:false, decision:'block', stopReason}
so the user saw:                   "Compaction blocked by PreCompact hook: [command]"  (no message)
and the aggregator then reported:  "PreCompact [command] failed"  (a deliberate block!)

Both blockers now set reason AND stopReason. decision:'block' is kept deliberately: continue:false alone does not set blocked on this path, so dropping it would silently disable the block.

The reader whose file never existed (#3321 claim 5)

pre-compact-saver read   .claude/feedback/token-budget-state.json   {estimatedUsed, ...}
the only writer emits    {tmpdir}/claude-session-<sid>/instruction-budget.json
                         {version, files: {path: bytes}, warned}

Not a path rename: the expected SCHEMA never existed either. The saver now derives estimatedUsed from the real writer's byte census.

Why the tests never caught any of this

Fixtures built from types, types describing a payload that does not exist

Seven tests hand-fed idle_duration_ms: 31000 into a hook and asserted the >30s branch fired. CC cannot produce that input, so the branch never fired in production, while the test proved it "worked". Same closed loop as writeShellCwdState() in #3411: a test writing the state no writer produces validates half a contract and reports it as whole. The rewritten tests pin the real payload shape, including one asserting the fabricated field is ABSENT from telemetry. The structural gap remains: EPIC B gates hook output keys against the binary; nothing yet gates input payload reads. That is the follow-up this PR sets up.

Verification

typecheck                        exit 0
hook TS suite                    311 files · 7641 passed · 2 skipped
hooks + plugin build             exit 0, zero drift beyond edited files
payload builders                 extracted verbatim from 2.1.227 for all 5 events

Scope note. All the legacy field reads are kept as trailing fallbacks, not deleted: the sweep verified against 2.1.227 only, and the support floor is 2.1.220. If any floor version sent the old names, the fallbacks cover it; when the floor rises past the doubt, they can go.