[FEATURE] Hook visibility for tool calls rejected at input validation (Edit old_string not found): PreToolUse before validation, or a rejection event with additionalContext

Status Open
Reported on v2.1.258
Maintainer reply None cached
Activity 1 comment · opened Sep 10, 2026

Preflight Checklist

  • [x] I have searched existing issues: #77735 (maintainer comment confirms input validation runs before PreToolUse), #72996 and #24908 (PostToolUseFailure does not fire for built-in tool_use_error rejections). This is the feature request those threads point at, not a re-report of the behaviour.
  • [x] This is a single feature request.
  • [x] Reproduced on the latest version at the time of filing (2.1.258).

What would you like?

A hook-visible point for tool calls that Claude Code rejects at input validation, most importantly Edit calls whose old_string is not found. Any one of these would do, in order of usefulness:

  1. Run PreToolUse before input validation, so a hook can repair the input with updatedInput (for Edit: rewrite old_string) and validation then runs on the repaired input.
  2. Fire PostToolUseFailure (or a new event such as ToolInputRejected) when validation rejects a call, with tool_input, the rejection message, and additionalContext support, so a hook can tell the model why and what to do instead.
  3. At minimum, document the ordering in the hooks reference, as #77735's maintainer reply describes it: "Input validation runs before PreToolUse hooks. When a tool call is rejected at input validation, it never reaches the hook stage."

Why

We build a content-sanitization layer for coding agents that runs as Claude Code hooks. PreToolUse on Read swaps a sanitized view of the file in for the duration of the read (hidden Unicode such as zero-width characters, bidi overrides and tag characters removed), PostToolUse restores the original. The model therefore quotes old_string from the sanitized view. When the quoted span contains a hidden character on disk, Edit rejects it with "String to replace not found in file" before any hook runs, so the hook that could map old_string back onto the bytes on disk never gets the chance, and no post event fires either.

Measured with Claude Code 2.1.258 on Windows 11 (Git Bash), headless claude -p --model claude-haiku-4-5-20251001 --allowedTools Read,Edit --permission-mode acceptEdits, project-scope settings with a logging hook:

{"hooks": {
  "PreToolUse": [{"matcher": "Edit", "hooks": [{"type": "command",
    "command": "\"C:/path/python.exe\" -c \"import sys; open('edit-hook.log','a').write(sys.stdin.read()+chr(10))\""}]}],
  "PostToolUseFailure": [{"matcher": "Edit", "hooks": [{"type": "command",
    "command": "\"C:/path/python.exe\" -c \"import sys; open('edit-failure.log','a').write(sys.stdin.read()+chr(10))\""}]}]
}}

Across two sessions the model issued five Edit calls on a file that contains U+200B characters. Four had an old_string spanning them and were rejected with String to replace not found in file; one matched the disk and succeeded. edit-hook.log received exactly one event (the successful call); edit-failure.log received none.

The same ordering applies to File has not been read yet.

Workaround today

After a sanitized Read, our PostToolUse hook returns additionalContext with the line and column of every hidden span, and the model edits around them or rewrites the file with Write. To make edits through hidden text land we are moving to keeping the sanitized view on disk until the model's next command-running tool call (a longer exposure window than we would like). Option 1 or 2 above would remove the need for either.

Environment

  • Claude Code 2.1.258, native install, Windows 11 Pro (10.0.26200), Git Bash as the shell
  • Model in the repro: claude-haiku-4-5-20251001

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗