#3321 · hooks · silent no-op

The tests passed because they used the wrong field too

Two hooks read tool_output / tool_result — the legacy aliases. CC sends tool_response. Both saw an empty string on every real payload, and both failed quietly. Their test suites were green the whole time, because every case supplied the alias as well.

What each hook read

types.ts:58 literally says "legacy alias for tool_response".

sitereadson a real payload
mcp-output-transform.ts:306 tool_response ?? tool_output ?? tool_result correct already
token-estimator.ts:131 { tool_result, tool_output, output } '' → 0 tokens
secret-handler.ts:56 tool_output ?? tool_result '' → silent success

The repo had already learned this once. The comment above mcp-output-transform.ts:306 reads "CC sends the result in tool_response (verified on CC 2.1.183) … reading only those is why". The lesson was written down next to one call site and never carried to the other two.

Two different failure shapes

token-estimator      '' ──▶ estimateTokens('') = 0
                            └─▶ cache-read accumulator never increments
                                ├─ context-crossing-warn        reads 0
                                └─ pre-compact-task-done-prompt reads 0
                            every context-pressure heuristic blind

secret-handler       '' ──▶ if (!outputStr) return silentSuccess()   :58
                            └─▶ redaction control scanned nothing
                                while its own suite stayed green

The estimator one is a measurement failure — numbers read 0 and look like calm. The secret-handler one is a control failure: a hook whose whole job is to catch credentials in tool output was handed an empty string and agreed there was nothing to redact.

Why the suites were green

token-estimator.test.ts   tool_result · tool_result{content} · output · tool_output
secret-handler.test.ts    tool_output × every single case

  not one test supplied tool_response
  → the tests exercised a code path production never takes

This is the failure mode worth remembering: a green suite is only evidence about the inputs it actually sends. Both files tested the shape the code happened to read, so the tests and the bug agreed with each other.

The fix, and its proof

Both sites now use the same ordering as mcp-output-transform: preferred field first, aliases as fallbacks. Nothing that works today changes.

8 new cases run against origin/main's source:  8 FAIL
same 8 against this branch:                    8 PASS

  includes: estimateTokens(extract({tool_response: text})) > 0
            — the regression as a number, not a shape

Deliberately still open

#3321 lists five findings. This closes one. The other four — post-compact-recovery's additionalContext (PostCompact has no such support), the StatusLine bridge file with zero writers, pre-compact-saver reading token-budget-state.json while the writer emits instruction-budget.json, and both PreCompact blockers setting stopReason where CC reads reason — are separate defects, and several depend on the autoCompactWindow decision in #3325, which is still open.

Find other hooks reading a field the platform doesn't send.