Skip to content

[BUG] updatedInput PreToolUse response does not work when multiple PreToolUse hooks are executed #15897

@werdnum

Description

@werdnum

Preflight Checklist

  • I have searched existing issues and this hasn't been reported yet
  • This is a single bug report (please file separate reports for different bugs)
  • 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, updatedInput ignored)

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

  1. 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)

  1. Configure in settings.json:

{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{"type": "command", "command": "python3 /path/to/hook.py"}]
}]
}
}

  1. Run: echo "run: echo foo" | claude -p

  2. Expected: Output shows "bar"

  3. 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 updatedInput on 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)

  1. Configure in settings.json:

{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{"type": "command", "command": "python3 /path/to/hook.py"}]
}]
}
}

  1. Run: echo "run: echo foo" | claude -p

  2. Expected: Output shows "bar"

  3. 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 updatedInput on Grep tool

Steps to Reproduce

  1. 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)
  1. Configure in settings.json:
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{"type": "command", "command": "python3 /path/to/hook.py"}]
    }]
  }
}
  1. Run: echo "run: echo foo" | claude -p

  2. Expected: Output shows "bar"

  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions