PreToolUse `updatedInput` from an `allow` hook is silently discarded when a sibling hook returns `ask` and the user approves
Environment
- Claude Code 2.1.204
- macOS (darwin)
- Two PreToolUse hooks registered on the same matcher in
settings.json
Summary
When two PreToolUse hooks match the same tool call, and hook A returns permissionDecision: "allow" with updatedInput while hook B returns permissionDecision: "ask", approving the resulting permission prompt executes the ORIGINAL tool input. Hook A's updatedInput mutation is dropped, and neither the user nor hook A gets any signal that this happened. In effect, one hook asking for confirmation silently cancels another hook's input rewrite.
Reproduction
- Register two PreToolUse hooks on the same matcher (reproduced on an MCP tool matcher):
hook-a.sh (mutating hook):
``json``
{"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "allow", "permissionDecisionReason": "content rewrite", "updatedInput": {"...": "tool_input with one field modified"}}}
hook-b.sh (confirming hook):
``json``
{"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "ask", "permissionDecisionReason": "please confirm"}}
Registered together on one matcher:
``json``
{"hooks": {"PreToolUse": [{"matcher": "<your tool matcher>", "hooks": [
{"type": "command", "command": "/path/hook-a.sh"},
{"type": "command", "command": "/path/hook-b.sh"}
]}]}}
- Trigger a matching tool call whose input hook A will modify.
- Approve the permission prompt raised by hook B.
- Inspect what actually executed: the tool's side effect (e.g. content stored by the call) and the PostToolUse
tool_inputboth carry the ORIGINAL input, not hook A'supdatedInput.
Control arm: remove hook B (so no ask fires) and repeat. Hook A's updatedInput IS applied: the side effect and PostToolUse tool_input carry the modified input.
Verified via a controlled A/B on the same payload, comparing SHA-1 digests of the content the tool actually persisted: no-ask arm stored the modified bytes; ask-plus-approval arm stored the original bytes.
Expected
Either of these would be consistent behavior:
- The approved call executes with the sibling
updatedInputapplied (approval approves the call, mutations still compose), or - The documentation for multi-hook precedence (
deny > defer > ask > allow) states explicitly that whenaskwins and the user approves, siblingupdatedInputmutations are discarded.
Actual
The original input executes silently. The mutating hook has already reported success (its own stdout said allow + updatedInput), so from its perspective the mutation happened. There is no warning, no log line, and no way for hook A to detect the drop at PreToolUse time.
Impact
Content-rewriting hooks (sanitizers, scrubbers, redactors) silently fail exactly when another hook asks for confirmation, which in practice correlates with the higher-risk calls. If the mutation is a policy control, this is a silent policy bypass: telemetry from the mutating hook claims the rewrite happened while the unmodified payload goes through.
At minimum this looks like a documentation gap on the multi-hook precedence section; ideally updatedInput from an allow-voting hook would survive an approved ask.
Related: #66203 asks for the multi-hook semantics (execution order, updatedInput visibility, conflict resolution) to be specified in general. This report is one specific, reproduced data point for that specification: ask-wins-then-approve currently discards sibling updatedInput silently.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗