[FEATURE] Mods: let a session.append hook redact or drop any block of a tool-result row (some blocks are restored by the engine)
Preflight Checklist
- [x] I have searched existing requests and this feature hasn't been requested yet
- [x] This is a single feature request (not multiple features)
Problem Statement
A mod that redacts secrets from tool results before they reach the model and the transcript (a session.append hook on door tool-result that rewrites the row's content and passes it on with next) cannot remove a secret from some parts of a row, because the engine restores those parts whatever the hook sends.
Observed on Claude Code 2.1.296 through the hooks-module test kit (claude plugin test, claude-code/testing):
- A top-level
search_resultblock beside thetool_result: rewriting it, or dropping it fromcontent, has no effect. The block is stored as it arrived. - An unknown top-level block kind (and
thinking/tool_useblocks at the top level of a tool-result row): restored the same way. - Extra fields on a
tool_resultblock (anything beyond its known fields): restored even when the hook's rewrite of the same block'scontentlands.
We confirmed it is the engine, not our hook: a second test hook registered beneath ours, which removed the block or field outright, still saw it stored. By contrast, text blocks, nested search_result / document blocks inside tool_result.content, and text blocks' extra fields are rewritten or dropped as sent.
So a secret that lands in one of those parts reaches the transcript and the model in plaintext, and the hook has no way to stop it. SessionAppendResult does not let a plugin refuse a tool-result row either (only its own note appends). Our current behaviour is to detect the put-back after next (by comparing the stored row with what we sent), not count it as a redaction, and raise a notice that the block could not be redacted. That keeps the plugin honest but leaves the secret in place.
Proposed Solution
Let a session.append hook on door tool-result rewrite or drop any block in the row, and any field on a block, the same way it already can for text blocks and nested blocks. Redaction is the use case: a hook that has decided a block holds a secret needs the change to land.
If some blocks must stay pinned for the engine's own bookkeeping, either of these would also work for us:
- Let the hook replace a pinned block's text-bearing fields (keeping its structural fields), or
- Let the hook withhold the whole row's content for a
tool-result(for example{ withhold: reason }, replacing the content with a notice the model reads), so a plugin can fail closed when it cannot redact in place.
Alternative Solutions
- Document which blocks and fields of a
tool-resultrow are pinned, and expose that on the hook input (for example apinnedflag per block), so a plugin can at least know beforenextwhich redactions will not land.
Priority
Medium - Would be very helpful
Feature Category
Developer tools/SDK
Use Case Example
- A tool returns a result whose content holds a
tool_resultplus a top-levelsearch_resultblock, and thesearch_resulttext includes an access token. - The redaction mod's
session.appendhook replaces the token with[REDACTED:<kind>]in every block and callsnext. - Today the
tool_resulttext is stored redacted, but thesearch_resultis stored as it arrived, token included, and the model reads it. - With the change, the stored row carries
[REDACTED:<kind>]in both blocks.
Additional Context
- Claude Code 2.1.296 (mods / hooks modules), macOS. Observed in the test kit; not yet confirmed in a live session.
- Unrelated to settings
PostToolUsehooks; this is about function hooks in a mod. - Related mods API requests: #99417, #91870.