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

Status Open
Reported on v2.1.163
Maintainer reply None cached
Activity 6 comments · opened Jun 17, 2026

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 a 2.1.177 desktop-app session
  • macOS (Darwin 25.5.0), arm64
  • Hooks: project-scope .claude/settings.json, --setting-sources project

🤖 Filed via Claude Code

View original on GitHub ↗

4 Comments

omar-y-abdi · 1 month ago

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):

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {"type": "command", "command": "python3 /abs/path/replace_hook.py", "timeout": 10}
        ]
      }
    ]
  }
}
# replace_hook.py
import json, sys
json.load(sys.stdin)
print(json.dumps({"hookSpecificOutput": {
    "hookEventName": "PostToolUse",
    "updatedToolOutput": "REPLACED-BY-MINIMAL-HOOK"}}))

Ran echo hello-from-probe-test-12345 in 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, the tool_result block the model received is byte-for-byte the ORIGINAL:

TOOL_RESULT: 'hello-from-probe-test-12345'

updatedToolOutput was 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 transcript tool_result for that call too).

Two possibly-useful contrasts, same build (2.1.207):

  1. PreToolUse hookSpecificOutput.updatedInput works — a hook rewriting tool_input.command is honored (the rewritten command runs; verified in the transcript).
  2. SessionStart systemMessage and PostToolUse hook execution itself both work — it is specifically the application of updatedToolOutput to the model-visible result that is dropped.

The current docs at https://code.claude.com/docs/en/hooks still state: "PostToolUse: updatedToolOutput replaces the tool's result." — so either the docs or the behavior needs the fix. Happy to provide the full transcript excerpts if useful.

itdove · 1 month ago

Workaround: additionalContext as alternative to updatedToolOutput

We tested a workaround that avoids updatedToolOutput entirely by using additionalContext in hookSpecificOutput, which is reliably delivered to the agent on PostToolUse — including when decision: "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:

{
  "decision": "block",
  "reason": "Secret detected and redacted. Redacted version in context.",
  "systemMessage": "Secret detected and redacted. Redacted version in context.",
  "hookSpecificOutput": {
    "hookEventName": "PostToolUse",
    "additionalContext": "REDACTED OUTPUT: The command returned 3 lines of output. Sensitive value replaced: TOKEN=****"
  }
}

Result: Raw output is suppressed (block), agent receives the redacted version via additionalContext and can continue working.

Test reproduction

Minimal PostToolUse hook (/tmp/test-hook.py):

#!/usr/bin/env python3
import json, sys

hook_input = json.loads(sys.stdin.read())
output = hook_input.get("tool_response", {}).get("stdout", "")

if "test-additional-context" in output:
    print(json.dumps({
        "decision": "block",
        "reason": "TEST: Output blocked. Redacted version in context.",
        "systemMessage": "TEST: Output blocked. Redacted version in context.",
        "hookSpecificOutput": {
            "hookEventName": "PostToolUse",
            "additionalContext": "REDACTED OUTPUT: The command returned 3 lines of output. Sensitive value replaced: TOKEN=****"
        }
    }))
else:
    print("{}")

Hook config in ~/.claude/settings.json:

"PostToolUse": [
  {
    "matcher": "*",
    "hooks": [{ "type": "command", "command": "python3 /tmp/test-hook.py" }]
  }
]

Agent ran echo test-additional-context. Claude Code displayed:

PostToolUse:Bash hook returned blocking error
TEST: Output blocked. Redacted version in context.

Agent response confirmed it received the additionalContext:

Hook flagged sensitive content in output — token value redacted to TOKEN=****.

The agent paraphrased the redacted content — confirming additionalContext is delivered even when decision: "block".

Limitation for non-sensitive corrections: Without a working updatedToolOutput, there's no way to replace tool output for non-sensitive fixes (formatting, annotations). additionalContext adds alongside the original — the agent sees both versions. For sensitive content the block+context pattern above is a clean solution, but updatedToolOutput is still needed for clean output replacement without blocking.

Tested with: Claude Code (confirmed), Augment, Codex (inherit Claude Code format), Gemini CLI.

bradfeld · 1 month ago
[!WARNING] This comment is incorrect — see my correction below: https://github.com/anthropics/claude-code/issues/68951#issuecomment-5026447164 My updatedToolOutput payload was a bare string; for the Bash tool it must be an object ({stdout, stderr, interrupted}). With the object form, redaction works on v2.1.215. I also retract the decision:"block" endorsement at the end — it does not withhold Bash output. Leaving the original text below unedited for the record.

---

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 Bash hook that masks secret-shaped tokens in tool output via hookSpecificOutput.updatedToolOutput — a DLP/redaction use case, same family as the secrets scenario discussed above.

Verified this session (v2.1.215):

  1. The redaction hook is provably the last Bash-matching PostToolUse hook, so "only the last matching hook's hookSpecificOutput is 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
``

  1. Live echo of a fake Slack-shaped token (xoxb- + 16 chars) returned the token raw / unmasked in the model-visible result.
  2. The hook emits docs-correct JSON when fed the exact live payload shape it receives — so the hook is not the problem, application is:

``
$ 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) Bash PostToolUse hook — yet the model receives the original unmasked output. The docs at https://code.claude.com/docs/en/hooks still state updatedToolOutput replaces the tool's result, so behavior and docs remain out of sync on 2.1.215.

Confirming @itdove's decision:"block" + additionalContext pattern is the workaround we'll adopt for the redaction use case in the meantime — thanks for documenting it.

bradfeld · 1 month ago

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 updatedToolOutput payload was malformed

I reported emitting "docs-correct" JSON and quoted this:

{"hookSpecificOutput":{"hookEventName":"PostToolUse","updatedToolOutput":"[REDACTED: Slack token] ..."}}

The nesting is right; the value type is not. For the built-in Bash tool, updatedToolOutput has to be an object mirroring the tool's own result{stdout, stderr, interrupted} — not a bare string:

{"hookSpecificOutput":{"hookEventName":"PostToolUse",
  "updatedToolOutput":{"stdout":"[REDACTED: Slack token] ...","stderr":"","interrupted":false}}}

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 stdout and stderr — 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 redaction

I closed by saying I'd adopt @itdove's decision:"block" + additionalContext pattern. That does not do what I assumed, and I'd retract the endorsement. Measured on 2.1.215 with fake xoxb- and ghp_ tokens: the raw output still reached the model's context, while my hook's own reason string 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: PostToolUse fires after the tool result exists, so block cannot retract it — it appears to only inject reason as 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 hookSpecificOutput payload 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.

Showing cached comments. Read the full discussion on GitHub ↗