PostToolUse `updatedToolOutput` silently ignored for built-in Bash tool (2.1.163/2.1.177) — regression; prior reports #65403/#67442/#54196 closed as duplicate, never fixed
Summary
hookSpecificOutput.updatedToolOutput (PostToolUse output rewrite — documented, and shipped for all tools in v2.1.121) is silently ignored for the built-in Bash tool. A PostToolUse hook that returns a correctly-shaped envelope with exit 0 runs (confirmed via side-effects), but the model still receives the original tool output, not the replacement.
Reproduced on 2.1.163 (standalone CLI, minimal repro below) and observed in an interactive 2.1.177 session.
Why this is filed despite prior reports
This exact defect has been reported repeatedly and every report was auto-closed as duplicate by the bot, funneled into secret-redaction feature requests — so there is currently no open, canonical bug for it and it remains unfixed:
- #54196 (v2.1.121) — closed as duplicate
- #65403 (Bash, secret leak) — closed as duplicate of #65122
- #65122 — closed as duplicate
- #67442 (v2.1.173, Bash + WebFetch) — closed as duplicate of #65403
The open issues it gets merged into (#64326, #62156, #18653, #66044) are framed as new feature requests for output redaction. This is not a feature request: updatedToolOutput is a documented, shipped feature that is broken for built-in tools. Distinguishing the two is the point of this report.
Minimal reproduction
.claude/settings.json:
{
"hooks": {
"PostToolUse": [
{ "matcher": "Bash", "hooks": [ { "type": "command", "command": "bash /tmp/uto-min/hook.sh" } ] }
]
}
}
/tmp/uto-min/hook.sh (exit 0, valid envelope per docs):
#!/bin/bash
cat > /dev/null
printf '%s\n' '{"hookSpecificOutput":{"hookEventName":"PostToolUse","updatedToolOutput":"REDACTED_BY_HOOK"}}'
Run:
cd /tmp/uto-min && claude -p \
"Use the Bash tool to run exactly: echo SECRET_ORIGINAL_VALUE . Then reply with ONLY the exact verbatim stdout string the tool returned to you." \
--setting-sources project --allowedTools "Bash(echo:*)" --model haiku
Expected
The model receives REDACTED_BY_HOOK (the hook's updatedToolOutput), so it replies REDACTED_BY_HOOK.
Actual
The model replies SECRET_ORIGINAL_VALUE — the original output. The hook ran (its envelope is emitted on stdout, exit 0), but the rewrite was not applied.
Scope notes (from isolating the channels)
Tested side-by-side with sentinel hooks (all hooks confirmed firing via marker files) on the same version:
| Channel | Reaches the model? |
|---|---|
| PreToolUse hookSpecificOutput.additionalContext | ✅ yes (Bash and Edit) |
| PostToolUse hookSpecificOutput.additionalContext | ✅ yes |
| PreToolUse stderr + exit 2 (block reason) | ✅ yes |
| PostToolUse hookSpecificOutput.updatedToolOutput | ❌ no |
So additionalContext works on this version — it is specifically the output-replacement channel that is dropped. Because additionalContext can only append (never shrink/replace), there is no workaround: redaction and output-size reduction both depend on updatedToolOutput.
Worth checking whether it still works for MCP tools (the pre-2.1.121 scope) and regressed only for built-in tools (Bash/WebFetch) — that would match the v2.1.121 extension being the regression surface.
Impact
Any PostToolUse output transform for built-in tools is inert: secret/credential redaction (the recurring leak reports) and tool-output token-compression hooks both silently no-op while appearing to work (the hook reports success; the model still gets the full/unredacted output).
Environment
- Claude Code
2.1.163(standalone CLI, repro above); also observed in a2.1.177desktop-app session - macOS (Darwin 25.5.0), arm64
- Hooks: project-scope
.claude/settings.json,--setting-sources project
🤖 Filed via Claude Code
Showing cached comments. Read the full discussion on GitHub ↗
4 Comments
Still reproducible on v2.1.207 (macOS 24.6.0), with transcript-level verification and two data points that may help narrow it:
Minimal repro (project
.claude/settings.json, no plugins involved):Ran
echo hello-from-probe-test-12345in a fresh interactive session. The hook demonstrably executes (side effects observed; exit 0; clean single-line JSON on stdout). Reading the session's transcript JSONL afterwards, thetool_resultblock the model received is byte-for-byte the ORIGINAL:updatedToolOutputwas silently dropped. Same result with a larger real-world hook (compresses 23 KB of repetitive log output to 0.5 KB — replacement never reaches the model, verified in the transcripttool_resultfor that call too).Two possibly-useful contrasts, same build (2.1.207):
PreToolUsehookSpecificOutput.updatedInputworks — a hook rewritingtool_input.commandis honored (the rewritten command runs; verified in the transcript).SessionStartsystemMessageand PostToolUse hook execution itself both work — it is specifically the application ofupdatedToolOutputto the model-visible result that is dropped.The current docs at https://code.claude.com/docs/en/hooks still state: "
PostToolUse:updatedToolOutputreplaces the tool's result." — so either the docs or the behavior needs the fix. Happy to provide the full transcript excerpts if useful.Workaround:
additionalContextas alternative toupdatedToolOutputWe tested a workaround that avoids
updatedToolOutputentirely by usingadditionalContextinhookSpecificOutput, which is reliably delivered to the agent on PostToolUse — including whendecision: "block"is set.Use case: sensitive content (secrets, PII)
When tool output contains secrets that must not reach the agent raw, return block + redacted content in
additionalContext:Result: Raw output is suppressed (
block), agent receives the redacted version viaadditionalContextand can continue working.Test reproduction
Minimal PostToolUse hook (
/tmp/test-hook.py):Hook config in
~/.claude/settings.json:Agent ran
echo test-additional-context. Claude Code displayed:Agent response confirmed it received the
additionalContext:The agent paraphrased the redacted content — confirming
additionalContextis delivered even whendecision: "block".Limitation for non-sensitive corrections: Without a working
updatedToolOutput, there's no way to replace tool output for non-sensitive fixes (formatting, annotations).additionalContextadds alongside the original — the agent sees both versions. For sensitive content the block+context pattern above is a clean solution, butupdatedToolOutputis still needed for clean output replacement without blocking.Tested with: Claude Code (confirmed), Augment, Codex (inherit Claude Code format), Gemini CLI.
---
Still reproducible on v2.1.215 (macOS, Darwin 25.5.0) — extends the confirmed-broken range past the 2.1.207 report above, with an isolation angle that rules out multi-hook ordering/merge semantics as the cause.
Context: a PostToolUse
Bashhook that masks secret-shaped tokens in tool output viahookSpecificOutput.updatedToolOutput— a DLP/redaction use case, same family as the secrets scenario discussed above.Verified this session (v2.1.215):
Bash-matching PostToolUse hook, so "only the last matching hook'shookSpecificOutputis honored" is ruled out as an explanation:``
``$ jq -r '.hooks.PostToolUse|to_entries[]|select(.value.matcher|test("Bash"))|.value.hooks[].command' settings.json | tail -1
bash .../redact-secret-output.sh
echoof a fake Slack-shaped token (xoxb-+ 16 chars) returned the token raw / unmasked in the model-visible result.``
``$ jq -nc '{tool_name:"Bash",tool_response:{stdout:"xoxb-AAAAAAAAAAAAAAAA\n"},hook_event_name:"PostToolUse"}' | bash redact-secret-output.sh
{"hookSpecificOutput":{"hookEventName":"PostToolUse","updatedToolOutput":"[REDACTED: Slack token] ..."}}
So the hook fires, receives the correct shape, emits the documented
{"hookSpecificOutput":{"hookEventName":"PostToolUse","updatedToolOutput":"..."}}, and is the last (and effectively sole relevant)BashPostToolUse hook — yet the model receives the original unmasked output. The docs at https://code.claude.com/docs/en/hooks still stateupdatedToolOutputreplaces the tool's result, so behavior and docs remain out of sync on 2.1.215.Confirming @itdove's
decision:"block"+additionalContextpattern is the workaround we'll adopt for the redaction use case in the meantime — thanks for documenting it.Correction to my comment above — my repro was wrong, and the workaround I endorsed does not work. Retracting both, with what I found instead.
1. My
updatedToolOutputpayload was malformedI reported emitting "docs-correct" JSON and quoted this:
The nesting is right; the value type is not. For the built-in
Bashtool,updatedToolOutputhas to be an object mirroring the tool's own result —{stdout, stderr, interrupted}— not a bare string:With the object form, redaction works on v2.1.215. Same hook, same registration, same machine as my original report. I verified it live across Slack/GitHub/AWS/Anthropic-shaped fake tokens in both
stdoutandstderr— the raw values no longer reach the model.So for my case this was not a platform bug. A bare string in that slot is silently ignored — no error, no warning, no diagnostic — which is indistinguishable from the feature being broken. That's what sent me (and two working sessions) down the wrong path.
2.
decision:"block"does not withhold Bash output — do not adopt it for redactionI closed by saying I'd adopt @itdove's
decision:"block"+additionalContextpattern. That does not do what I assumed, and I'd retract the endorsement. Measured on 2.1.215 with fakexoxb-andghp_tokens: the raw output still reached the model's context, while my hook's ownreasonstring claimed it had been withheld — the worst outcome for a DLP control, since it reports containment that did not happen.That looks structural rather than a bug:
PostToolUsefires after the tool result exists, soblockcannot retract it — it appears to only injectreasonas feedback alongside the original result. Anyone using this pattern for secret redaction should verify whether the raw value is still in context; in my testing it was.3. Scope of this correction
This explains my repro only. I can't speak to the other reports in this thread — if yours emits the object form and still isn't applied, that's a different finding than mine and this comment doesn't refute it.
One documentation note, offered constructively: the failure mode here is entirely silent. A malformed or wrongly-typed
hookSpecificOutputpayload is dropped with no stderr warning and no debug output, so from the hook author's side "my JSON is being ignored" and "the feature is broken" look identical. A warning on unrecognized or mistyped keys would have saved me this whole detour.Apologies for the noise on the thread.