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.
| event, payload verbatim from the binary | ork read | effect |
|---|---|---|
Elicitation {mcp_server_name, requested_schema, ...} | server_name, form_schema | secret-field block could not fire |
FileChanged {file_path, event} | changed_file_path | hook inert for all 4 matchers |
TeammateIdle {teammate_name, team_name} | teammate_type, idle_duration_ms | telemetry logged fabricated 0s as measured |
PostCompact {trigger, compact_summary} | compaction_count, context_size_after | silent fallbacks |
StopFailure {error, error_details, ...} | stop_failure_reason, api_status_code | reason=unknown |
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.
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.
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.
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.
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.