[BUG] updatedInput PreToolUse response does not work when multiple PreToolUse hooks are executed
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
I have been trying to set up a PreToolUse hook to modify bash commands (add timeout, rewrite known bad patterns) but no matter how hard I try, I can't get it to work.
See #9185, #4368
What Should Happen?
Updated Input should be respected
Error Messages/Logs
Claude's diagnosis:
Bug Report: updatedInput ignored in PreToolUse hooks
Environment
- Claude Code version: 2.0.76
- Platform: Linux (devcontainer)
Summary
PreToolUse hooks that return permissionDecision: "allow" with updatedInput have the updatedInput field completely ignored. The original tool input is executed instead of the modified input.
Blocking (exit code 2) works correctly. Only updatedInput with allow is broken.
Hook Script Output (correct)
When run manually:
$ echo '{"tool_name": "Bash", "tool_input": {"command": "echo foo"}}' | python3 /path/to/hook.py
stdout:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "Rewriting command: 'echo foo' → 'echo bar'\n Reason: TEST rule",
"updatedInput": {
"command": "echo bar"
}
}
}
The hook script exits with code 0 and outputs valid JSON with updatedInput.
Claude Code Streaming Output (shows bug)
$ echo "run: echo foo" | claude -p --debug hooks --output-format stream-json --verbose 2>&1
Tool call from Claude:
{"type":"assistant","message":{"content":[{"type":"tool_use","id":"toolu_01...","name":"Bash","input":{"command":"echo foo","description":"Print 'foo' to stdout"}}]}}
Tool result (shows original command ran, not modified):
{"type":"user","message":{"content":[{"tool_use_id":"toolu_01...","type":"tool_result","content":"foo","is_error":false}]},"tool_use_result":{"stdout":"foo","stderr":"","interrupted":false,"isImage":false}}
- Expected:
"stdout":"bar"(modified command should run) - Actual:
"stdout":"foo"(original command ran,updatedInputignored)
Blocking Mode Works (for comparison)
When hook returns exit code 2 with stderr message instead of JSON with allow:
{"type":"user","message":{"content":[{"type":"tool_result","content":"Hook PreToolUse:Bash denied this tool","is_error":true,"tool_use_id":"toolu_01..."}]},"tool_use_result":"Error: Hook PreToolUse:Bash denied this tool"}
This correctly blocks the tool and Claude sees the error. So hooks ARE running, just updatedInput is ignored.
Minimal Reproduction
- Create hook script that returns
updatedInput:
#!/usr/bin/env python3
import json, sys
input_data = json.load(sys.stdin)
if input_data.get("tool_name") == "Bash":
cmd = input_data.get("tool_input", {}).get("command", "")
if cmd == "echo foo":
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": {"command": "echo bar"}
}
}))
sys.exit(0)
sys.exit(0)
- Configure in settings.json:
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{"type": "command", "command": "python3 /path/to/hook.py"}]
}]
}
}
- Run:
echo "run: echo foo" | claude -p
- Expected: Output shows "bar"
- Actual: Output shows "foo"
Related Issues
- #4368 - Feature request (closed as completed in v2.0.10)
- #9185 - Documentation issue noting feature exists but undocumented
- #11113 - TypeError crash with
updatedInputon Grep tool
hook script that returns updatedInput:
#!/usr/bin/env python3
import json, sys
input_data = json.load(sys.stdin)
if input_data.get("tool_name") == "Bash":
cmd = input_data.get("tool_input", {}).get("command", "")
if cmd == "echo foo":
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": {"command": "echo bar"}
}
}))
sys.exit(0)
sys.exit(0)
- Configure in settings.json:
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{"type": "command", "command": "python3 /path/to/hook.py"}]
}]
}
}
- Run:
echo "run: echo foo" | claude -p
- Expected: Output shows "bar"
- Actual: Output shows "foo"
Related Issues
- #4368 - Feature request (closed as completed in v2.0.10)
- #9185 - Documentation issue noting feature exists but undocumented
- #11113 - TypeError crash with
updatedInputon Grep tool
Steps to Reproduce
- Create hook script that returns
updatedInput:
#!/usr/bin/env python3
import json, sys
input_data = json.load(sys.stdin)
if input_data.get("tool_name") == "Bash":
cmd = input_data.get("tool_input", {}).get("command", "")
if cmd == "echo foo":
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": {"command": "echo bar"}
}
}))
sys.exit(0)
sys.exit(0)
- Configure in settings.json:
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{"type": "command", "command": "python3 /path/to/hook.py"}]
}]
}
}
- Run:
echo "run: echo foo" | claude -p
- Expected: Output shows "bar"
- Actual: Output shows "foo"
Claude Model
Opus
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.0.76
Platform
Anthropic API
Operating System
Ubuntu/Debian Linux
Terminal/Shell
Other
Additional Information
_No response_
20 Comments
---
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
I had Claude do some reverse engineering. It seems like the problem occurs when there are multiple hooks for the same tool.
<details>
<summary>Claude's analysis</summary>
Bug Analysis:
updatedInputignored in PreToolUse hooksEnvironment
Summary
PreToolUse hooks that return
permissionDecision: "allow"withupdatedInputhave theupdatedInputfield ignored when multiple hooks are registered for the same tool. The original tool input is executed instead of the modified input.Root Cause
Found via code archaeology in
/home/claude/.npm-global/lib/node_modules/@anthropic-ai/claude-code/cli.js:The bug is in the hook result aggregation logic in the
msfunction. When multiple hooks run:permissionBehavior(allow/deny/ask) is correctly aggregated across hooks using a tracking variableEupdatedInputis NOT aggregated - each hook's yield uses only its ownz.updatedInputGu5) overwriteszwith eachhookPermissionResult:case"hookPermissionResult":z=j.hookPermissionResult;break;updatedInput: undefinedThe Buggy Code Pattern
What Happens With Multiple Hooks
Example: User hook + bash-guard plugin (2 hooks for "Bash" tool)
{permissionDecision: "allow", updatedInput: {command: "echo bar"}}Eis set to"allow"{permissionBehavior:"allow", updatedInput:{command:"echo bar"}}Gu5:z = {behavior:"allow", updatedInput:{command:"echo bar"}}z.permissionBehaviorisundefinedEstays"allow"(already set from hook 1)E !== void 0, it yields AGAIN:{permissionBehavior:"allow", updatedInput:undefined}(becausez.updatedInputfor THIS hook is undefined)Gu5:z = {behavior:"allow", updatedInput:undefined}- OVERWRITES HOOK 1's RESULTupdatedInputis lostReproduction
Minimal Hook Script (Node.js)
Configuration
Test Command
Debug Log Shows Hook Works Correctly
But Stream Output Shows Original Command Ran
Expected:
"stdout":"bar"Why It Works For Some People
The bug only manifests when multiple hooks are registered for the same tool matcher. If you have:
Most users testing
updatedInputprobably don't have plugins with PreToolUse hooks installed, so their single hook works fine.Proposed Fix
Track
updatedInputacross hooks similar to howpermissionBehavioris tracked:Related Issues
Workaround
Until fixed, users can:
</details>
Independent Confirmation + Additional Findings
I can confirm this bug on Claude Code 2.0.76 (macOS, Anthropic OAuth).
Test Setup
I created isolated tests with minimal hooks to verify the behavior:
Hook 1 (modifier): Returns
updatedInput: {command: "echo MODIFIED"}when it seesecho ORIGINALHook 2 (passthrough): Returns empty
{}Results
| Config Order | Result | Notes |
|--------------|--------|-------|
| Modifier only |
MODIFIED| Single hook works correctly ||
[modifier, passthrough]|MODIFIED| Modifier FIRST → works ||
[passthrough, modifier]|ORIGINAL| Modifier SECOND → broken |Key Finding: Position-Dependent, Not "Last Wins"
The bug behavior is more nuanced than "last hook overwrites." My testing shows:
updatedInputis preservedupdatedInputis silently discardedThis means even a passthrough hook that returns
{}(and doesn't touchupdatedInput) will break subsequent hooks that DO returnupdatedInput.Real-World Impact
I have 12 PreToolUse hooks on the Bash matcher. Three of them use
updatedInputto:-yflags for package managers (non-interactive mode)-vflags for file operations (verbose output)These hooks are at positions 3-5, with passthrough hooks at positions 1-2. Based on my testing, all three have been silently broken this entire time—commands execute as originally written despite the hooks outputting correct
updatedInputresponses.Workaround
Move all
updatedInputhooks to the beginning of the chain (position 1, 2, 3...). Passthrough hooks should come after.Limitation: Only the FIRST
updatedInputhook that fires will have its modification applied. MultipleupdatedInputmodifications cannot compose.Logs Confirming Hook Execution
My hooks log their input/output to files. The modifier hook IS being called and IS returning correct JSON:
Yet the executed command is
echo ORIGINAL. TheupdatedInputis being discarded somewhere in the aggregation logic.---
Environment:
This issue has been inactive for 30 days. If the issue is still occurring, please comment to let us know. Otherwise, this issue will be automatically closed in 30 days for housekeeping purposes.
I was still able to reproduce this issue today.
Another real-world case: graphical-sudo plugin
Hitting this exact bug with graphical-sudo, a plugin that intercepts
sudocommands and rewrites them to usesudo -Awith a zenity askpass for graphical password prompts on Linux.The hook output (correct per docs)
Confirmed: single hook works, multiple hooks breaks
updatedInputis silently ignored — the originalsudo lsruns unmodified.updatedInputworks perfectly — zenity dialog appears, graphical auth succeeds.permissionDecision: "allow"is respected in both cases (no permission prompt). OnlyupdatedInputis lost.defaultandbypassPermissionspermission modes.decision: "approve"format withupdatedInput— same result.Impact
This makes
updatedInputunusable in any real plugin ecosystem, since users will almost certainly have multiple plugins installed that register PreToolUse hooks (hookify, security-guidance, etc.). The feature only works in isolation.Hitting this as well. Reproducing reliably on v2.1.51, macOS,
--dangerously-skip-permissionsmode.Setup: 4 PreToolUse hooks match Bash tool calls — 2 with no matcher (debug logger, mutation blocker) and 2 with
"matcher": "Bash"(command rewriter usingupdatedInput, task stop blocker). The rewriter produces validhookSpecificOutputJSON withupdatedInputto rewrite commands (confirmed via separate file logging +xxdshowing clean{as first byte on stdout).Observed:
--debuglog shows all 4 hooks reportingHook output does not start with {, treating as plain text— including the hook that definitely outputs valid JSON. TheupdatedInputis applied ~1 in 5 times (race-dependent). The debug output doesn't identify which hook produced which output, making this very difficult to diagnose.Root cause confirmation: From the decompiled hook aggregation logic,
permissionBehavioris tracked in a persistent variable across hooks, butupdatedInputis read from each individual hook result — so the last hook processed overwrites it, and since hooks run in parallel, the order is non-deterministic.Workaround: Switched to
permissionDecision: "deny"with the rewritten command inpermissionDecisionReason, which forces a retry. Works reliably but adds a round-trip per rewrite. Also includingupdatedInputin the response for when this gets fixed.Would love to see either:
updatedInputaggregated/preserved across hooks (non-null wins over null)+1
Another real-world case: tokf + babel (daemon telemetry) collision
Hitting this on v2.1.50, Linux (Arch/CachyOS), bypass-permissions mode.
Setup
Two PreToolUse hooks, both matching Bash:
matcher: "Bash") — token filter that rewrites commands to compress output for LLM context. Returns validupdatedInputJSON:``
json
``{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow",
"updatedInput":{"command":"tokf run git status"}}}
matcher: "", catch-all) — OS daemon that indexes Claude sessions across terminals. Purely observational: sets aToolRunningstate in sqlite + pushes to daemon via IPC for live UI. Returns no output (exit 0).Result
tokf's
updatedInputis silently discarded 100% of the time. The original command executes unfiltered. Debug log confirms the rewrite is parsed correctly but never applied:Why this is deterministic (not a race)
tokf is a direct
execinto a Rust binary (~37ms). babel's hook goes throughbash→cat→ 2×jq→babelbinary (~96ms). tokf always finishes first, so babel's response is always the lastyieldin the aggregation loop, always clobberingupdatedInputwithundefined.This makes it worse than a flaky race — it's a guaranteed, silent, 100% failure that produces no error, no warning, and no indication that the rewrite was dropped. The hook system reports success.
Broader implications
This bug means any two PreToolUse hooks that match the same tool are fundamentally incompatible if either uses
updatedInput. The failure mode is silent — both hooks report success, but the rewrite never applies. This is especially insidious for the emerging ecosystem of hook-based tools (tokf, graphical-sudo, babel, etc.) because:Suggested fix
The aggregation loop should accumulate
updatedInputacross hooks (non-null wins over null, or deep-merge), not re-read it from each individual hook response. ThepermissionBehavioraggregation already does this correctly (deny > ask > allow) —updatedInputjust needs the same treatment.Alternatively, a
permissionDecision: "passthrough"that truly passes through (skips the yield entirely) would let observational hooks coexist with rewriting hooks without interference.any update on it ? Thanks.
I swear that hooks are the single most bug-ridden feature in all of Claude Code. It's crazy that the case of multiple hooks existing isn't already covered in tests, or at least fixed in the past few months since this issue was opened.
I just ran into this too. I was hoping to have a PreToolUse hook automatically fill in the session and agent ids for certain tool calls. Pity it doesn't work -
updatedInputfeels like a pretty powerful tool!I can confirm this bug still exists in Claude Code v2.1.81. I was trying to install rtk-ai/rtk for my Claude Code and encountered this issue. I spent all the quota I have for one session to finally find this bug (kind of going against the purpose of rtk. That's funny).
I call for Anthropic's attention on this bug.
Approve, we really need that to be fixed
/tmp/issue-15897-comment.md
Additional repro:
updatedInputignored even as the last/only rewriting hookClaude Code: 2.1.87
OS: macOS (Darwin 25.3.0, arm64)
Permission mode: bypassPermissions
Setup
safety-guard.sh(security checks, exits 0 on allow) →rtk-rewrite.sh(rewrites commands via RTK)What I did
Added debug logging to
rtk-rewrite.shthat writes every invocation to/tmp/rtk-hook-debug.log. Rangit statusvia Bash tool.Debug log output
Result
Claude Code ran the original
git status(16 lines of unfiltered output) instead ofrtk git status(compact 6 lines). The hook fired, produced valid JSON, and was completely ignored.Variations tested (all ignored)
updatedInputinsidehookSpecificOutputwithpermissionDecision: "allow"← documented formatupdatedInputinsidehookSpecificOutputwithoutpermissionDecisionupdatedInputat top level (outsidehookSpecificOutput)Why this matters beyond multi-hook aggregation
The original issue describes the last hook's undefined
updatedInputoverwriting earlier hooks. In my case, RTK is the last hook — there is nothing to overwrite it. This suggestsupdatedInputfor Bash commands is not being applied at all, not just a multi-hook ordering problem.Workaround
Shell functions in
~/.zshenvbypass the hook system entirely:.zshenvis sourced for all zsh invocations includingzsh -c -l(how Claude Code runs Bash commands).+1 I can confirm this is still a bug that needs fixing please!!!
+1 — hit this with RTK's PreToolUse rewrite hook: adding any second matching hook causes the first hook's updatedInput to be silently dropped.
Closing for now — inactive for too long. Please open a new issue if this is still relevant.
This appears to be fixed as of 2.1.168 (macOS), even though it was closed
not planned.Repro of the exact condition described here — a
BashPreToolUsechain with a rewrite hook (returnspermissionDecision: "allow"+updatedInput) followed by a passthrough hook (exit 0, no JSON), registered in that order, plus other passthrough hooks (guard plugins):updatedInputwas not overwritten by the trailing passthrough hook(s). That's the failure this issue reported, and it no longer reproduces.tool_input(no chaining) — so the survivingupdatedInputis applied at execution, not propagated hook-to-hook.So the aggregation bug analyzed above (a later passthrough's empty result clobbering an earlier hook's
updatedInput) seems resolved in current builds.The behavior is still undocumented, though, so it's only verifiable by reverse-engineering per version. I opened #66203 to request that the multi-hook
PreToolUsesemantics (execution order,updatedInputvisibility/precedence across hooks, conflict resolution) be documented so people don't have to rediscover this.