Claude Code updatedInput Hooks: 2.1.300 Change Tested
Claude Code 2.1.300 validates updatedInput from PreToolUse hooks. I compared 2.1.299 with 2.1.300 and 2.1.306 on unknown keys, bad types, paths and Edit.

Table of contents
Claude Code 2.1.300 changed how it treats updatedInput, the field a PreToolUse hook uses to rewrite a tool call. The changelog says it "is now validated like Claude's own input: unknown keys are dropped, and input the tool rejects denies the call".1
I ran the same hooks on 2.1.299 and 2.1.300, and the main cases again on 2.1.306, to see what changed in practice. Part of it did change. Part of what that sentence suggests was already true on 2.1.299.
TL;DR
- The docs say an
updatedInputrewrite replaces the tool's arguments. In my Write test it did not merge: a hook that returned onlyfile_pathwas denied on all three builds becausecontentwas missing. - Unknown keys are the clearest 2.1.300 change. On 2.1.299 an extra key reached the tool (I saw it in the
PostToolUseinput for Bash, Read, Write and Edit). On 2.1.300 and 2.1.306 it was dropped for all four. - Schema-invalid rewrites, such as a number where
commandshould be a string, were already denied on 2.1.299. 2.1.300 adds one sentence to the error: "The tool input from the model was valid; the hook's rewrite of it was not." - For Edit, 2.1.300 now refuses a bad rewrite and attributes the error to the
PreToolUsehook. On 2.1.299 a rewrite whoseold_stringmatched the file twice was applied to the first match. Two other bad rewrites, anold_stringmissing from the file and identical old and new strings, both failed with the generic Edit error "String not found in file." - 2.1.300 also turned a relative Write path into an absolute one. A Bash
timeoutof -5 still ran on both builds.
What You'll Learn
- What
updatedInputdoes and where it goes in the hook's JSON - How I tested it, and on which builds
- Which behaviors changed in 2.1.300 and which did not
- A rewrite hook that works on all three builds
- A short checklist for hooks that rewrite tool calls
What updatedInput does
The hooks reference puts it in one line: updatedInput "directly under hookSpecificOutput replaces a tool's arguments before it runs."2
The word that matters is replaces. A February 2026 GitHub issue was filed because the reporter found it "replaces the entire tool_input object" instead of merging.3 My test agrees with that reading. A Write rewrite with file_path but no content was denied on every build.
Placement trips people up too. A September 2026 write-up by mer.vin tested 2.1.282 and found that an updatedInput placed at the top level of the JSON, next to hookSpecificOutput, was ignored.4 I checked that with a Bash call on three builds and got the same result, covered below.
For related hook behavior, see our tests of onFailure block and the Claude Code mods guide.
How I tested it
I installed each build from npm into its own folder: 2.1.299, 2.1.300 and 2.1.306. The two older builds were published on October 14, 2026, and 2.1.306 on October 21, 2026, when its latest tag pointed at it.5
The first batch (valid rewrite, wrong types, empty object, timeout and run_in_background) ran on October 15, 2026 from the same npm installs of 2.1.299 and 2.1.300. The Read empty-object check also ran on October 15, in a second early batch. Every other run I report here was on October 21, 2026, on Linux x86_64 with Node.js 22.22.0.
Each run was a real claude -p call with the model claude-sonnet-5-5. I passed --settings pointing at a test file, --setting-sources "" to keep other config out, --max-turns 4, and --allowedTools set to the tool under test (Read and Edit together for the Edit cases).
That flag pre-approves tools but does not remove the others, so the model sometimes called Bash too. The prompt asked for one plain action: touch a marker file for Bash, read one small file, write one, or read a file and then edit it.
Each test had a PreToolUse hook that returned the updatedInput under test. In most runs a PostToolUse hook also wrote the tool_input it received to a log, so I could see what the tool really got.
That covers the unknown-key, relative-path and top-level cases, and the Edit cases where the call went through. A refused call never reaches the tool, so it has no PostToolUse entry.
The first batch (valid rewrite, wrong types, empty object, timeout, run_in_background) and the redirect hook runs had no such log. For those, and for every refused call, I checked marker files, the resulting file and the tool_result text in the stream. Where the table says 3 runs, all three agreed.
Results at a glance

Each row groups related hook return values. The columns show what happened on 2.1.299 and 2.1.300, built from the raw run output. The table below splits the cases out and gives run counts.
| Hook returned | 2.1.299 | 2.1.300 | Runs per build |
|---|---|---|---|
Valid command only (Bash) | Rewrite used | Rewrite used | 2 |
command plus unknown keys bogus_key and another (Bash) | Keys reached the tool | Keys dropped | 3, plus 306 |
Unknown key bogus (Read) | Kept | Dropped | 3, plus 306 |
Unknown key extra_key (Write) | Kept | Dropped | 3, plus 306 |
Unknown key extra (Edit) | Kept | Dropped, default replace_all: false added | 3, plus 306 |
command set to a number | Denied | Denied, one sentence added | 2 |
| Empty object (Bash, Read) | Denied, parameter missing | Denied, one sentence added | 2 for Bash, 1 for Read |
timeout as a string, description as a number (Bash, two separate hooks) | Denied | Denied, one sentence added | 2 each |
offset: -3 (Read) | Denied by schema | Denied by schema, one sentence added | 3, plus 306 |
timeout: -5 (Bash) | Ran, killed at once | Ran, killed at once | 2 |
run_in_background: "yes" (Bash) | Ran | Ran | 2 |
Relative file_path (Write) | Passed as written | Made absolute | 3 to 4, plus 306 |
Edit old_string matches twice, replace_all unset | Applied to the first match | Refused, error cites the hook | 3, plus 306 |
Edit old_string not in file | Generic tool error | Refused, error cites the hook | 3, plus 306 |
| Edit with identical old and new string | Generic "String not found" error | Refused, error cites the hook | 3, plus 306 |
updatedInput at the top level of the JSON | Ignored, original ran | Ignored, original ran | 3, plus 306 |
file_path only (Write) | Denied, content missing | Denied, content missing, one sentence added | 3, plus 306 |
Where I ran 2.1.306, it matched 2.1.300 in every case. That covers unknown keys on all four tools, the relative path, all three Edit refusals, the Read offset, the top-level placement and both redirect hooks.
Unknown keys: kept on 2.1.299, dropped on 2.1.300
This is the change the changelog describes, and I could see it.
On 2.1.299 the PostToolUse log for the Bash case held bogus_key and another. On 2.1.300 and 2.1.306 it held only command. Read, Write and Edit behaved the same way, on 2.1.306 as well.
In none of my cases did the extra key change what the tool did. The tools ignored it. For these four tools, a hook can no longer pass unknown keys into the tool call.
Our read: for a hook that builds its output from a template or from model-visible text, this is a small safety win. It does not replace checking the fields you do set.
The Edit case showed a second effect. On 2.1.300 and 2.1.306 the logged input also carried replace_all: false. On 2.1.299 that field was absent. I did not test whether that default explains the double-match result below.
Rejected rewrites: schema errors on both, Edit refusals on 2.1.300
The changelog says input the tool rejects "denies the call". For schema errors that was already true.
On 2.1.299, a rewrite that gave command a number, left command out, passed timeout as a string or passed description as a number came back as an error starting "PreToolUse hook for Bash returned updatedInput that failed schema validation". Nothing ran.
2.1.300 keeps that message and adds a sentence: "The tool input from the model was valid; the hook's rewrite of it was not." It tells Claude, and you, which side to fix.
The Edit cases are where behavior moved. These are errors the Edit tool raises about content, not the schema.

The tool_result text for an Edit rewrite whose old_string is not in the file, on 2.1.300.
With an old_string that is not in the file, 2.1.299 returned the Edit tool's own error, "String not found in file. Failed to apply edit." With identical old and new strings it returned the same message, which does not describe the real problem.
2.1.300 refused both and said the PreToolUse hook's rewrite was the cause. One error read "No changes to make: old_string and new_string are exactly the same." Each one ended with the same sentence blaming the rewrite. In these two cases the file was unchanged in every run on every build.
The third Edit case has real consequences. I put alpha beta beta in the file and asked Claude to replace alpha with gamma, which is a valid call. The hook rewrote it to replace beta with gamma and left replace_all unset. beta appears twice.
On 2.1.299 the Edit tool applied it. The result said the file "has been updated successfully", and the file read alpha gamma beta in all three runs. On 2.1.300 and 2.1.306 the rewrite was refused with "Found 2 matches of the string to replace, but replace_all is false." The file stayed alpha beta beta in all six runs.
One more observation, with a small sample. In the not-in-file and identical-string cases, all six runs on 2.1.299 hit my four-turn cap. Each held four tool calls: Read, the failing Edit, a second Read and a Bash command such as od -c on the file. On 2.1.300 all six runs finished, with two or three tool calls, and in three of them the model simply retried Edit.
That depends on the model, and four is a ceiling I set. An earlier batch used a prompt that made the model's own first Edit fail, and in that batch both builds mostly hit the cap, so I reran these cases. Treat the counts as a hint and not a measurement.
Paths: relative becomes absolute
I returned relative_w.txt as the Write file_path. On 2.1.299 the tool received it as written and the file landed in the working directory. On 2.1.300 and 2.1.306 the tool received the absolute path of the same location.
The practical effect is on logs and audit hooks. In my test, a PostToolUse hook saw the absolute path on 2.1.300 and 2.1.306 and the relative path on 2.1.299. Resolve paths against the cwd field of the hook input before you compare them. I did not test a relative path that came from the model, and I got no usable result for a relative path on other tools.
What did not change
These outcomes were the same on each build I ran them on.
A Bash timeout of -5 slipped through. It ran on 2.1.299 and 2.1.300, and the command was killed straight away with exit code 143. A run_in_background of the string "yes" ran too.
offset: -3 on Read was denied on all three builds, and the error quotes a minimum of 0 from Read's schema. So in this case the minimum came from Read's schema on every build. The only difference between builds was the usual extra sentence on 2.1.300 and 2.1.306.
Top-level placement is still ignored. I returned updatedInput as a sibling of hookSpecificOutput for a Bash call. The original command ran in all nine runs. With a debug file, each run logged "Hook JSON output had unrecognized keys (ignored): updatedInput."
A rewrite hook that works on all three builds
This hook sends every Write into a .claude-out folder under the working directory. It copies the whole input first, because the rewrite replaces it.
#!/usr/bin/env node
// PreToolUse hook for the Write tool: send every write into ./.claude-out/
const fs = require("fs");
const path = require("path");
const event = JSON.parse(fs.readFileSync(0, "utf8"));
const input = event.tool_input;
const target = path.join(event.cwd, ".claude-out", path.basename(input.file_path));
console.log(JSON.stringify({
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "allow",
// updatedInput replaces the whole input, so copy every field first
updatedInput: { ...input, file_path: target },
},
}));
The hook returns permissionDecision: "allow". I only ran it headless with the tool pre-approved, and I did not test ask or interactive mode, so choose the decision that fits your setup. Register it in your settings file:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"hooks": [
{ "type": "command", "command": "node /path/to/redirect-writes.js", "timeout": 10 }
]
}
]
}
}
I asked Claude to write notes.txt in a folder outside the project, with the content "original". On 2.1.299, 2.1.300 and 2.1.306, in 3 runs each, the file appeared as .claude-out/notes.txt with the right content, and nothing was written at the original path.
The same script with { file_path: target } as the whole updatedInput was denied in all nine runs, with "The required parameter content is missing".
A checklist for rewriting hooks
- Copy
tool_inputand change only the fields you mean to change. - Put
updatedInputinsidehookSpecificOutput, never beside it. - Test one rewrite per tool you match, with a
PostToolUsehook loggingtool_input, so you see what the tool received. - Do not rely on validation for values such as timeouts. On 2.1.299 it also did not stop a rewrite that matched the file twice. Check numbers, paths and strings in the hook.
- Pin the Claude Code version in CI. The changelog sections I read for 2.1.301 to 2.1.306 do not mention
updatedInput, but the 2.1.300 change shows the behavior can move.6 - Decide what should happen when the hook itself breaks. The onFailure tests cover that for command and HTTP hooks.
What I did not test
I did not test updatedInput on PermissionRequest hooks, where it sits inside a decision object.2 I did not test MCP tools, subagents, Windows, interactive mode, ask or defer decisions, async hooks, plugin hooks or managed settings.
I also tested only Bash, Read, Write and Edit. Other tools may normalize or reject input in their own ways.
Two hooks returning different updatedInput for the same call is untested as well. The docs excerpt I read did not state a precedence rule.
For the human-approval side of agent tool calls, see our human-in-the-loop guide.
Bottom Line
On the builds I ran, updatedInput behaves as the docs say: it replaces the input. The 2.1.300 change shows up as dropped unknown keys, a normalized Write path and refusals for Edit rewrites that 2.1.299 either failed with a generic error or, in one case, applied.
Schema-level denial was already there on 2.1.299, so the new sentence in the error is a small part of the change.
Copy the full input, nest the field under hookSpecificOutput, and log what the tool receives. Then test one bad rewrite per tool on the build you ship.
References
Frequently Asked Questions
What is updatedInput in a Claude Code hook?
It is a field a PreToolUse hook returns to replace a tool's arguments before the tool runs. It goes directly under hookSpecificOutput.
Does updatedInput merge with the original input?
No, in my test. The docs say it replaces the arguments. In my Write test, a rewrite with file_path but no content was denied on 2.1.299, 2.1.300 and 2.1.306.
Why is my updatedInput ignored?
Check the placement first. In my Bash test, at the top level of the JSON next to hookSpecificOutput, it was ignored on all three builds, and a debug file logged "Hook JSON output had unrecognized keys (ignored): updatedInput." The mer.vin write-up reported the same placement problem on 2.1.282.
What changed for updatedInput in Claude Code 2.1.300?
The changelog says it is validated like Claude's own input: unknown keys are dropped and rejected input denies the call. In my tests, unknown keys were dropped, a relative Write path became absolute, Edit rewrites that the tool refuses, including one that 2.1.299 had applied, were refused with the error attributed to the hook, and schema errors gained an extra sentence.
Were invalid rewrites allowed on 2.1.299?
Schema-invalid ones were denied: on 2.1.299 a wrong type for command, timeout or description, or a missing required field. The string "yes" for run_in_background was accepted and the command ran. I did not check how Claude Code interpreted it. Edit rewrites that fail on content did not get the same treatment. Text not in the file, or identical old and new strings, came back as the tool's own generic error. A rewrite that matched the file twice was applied to the first match.
Does 2.1.300 check that numbers are in range?
Not for the Bash timeout in my tests: -5 ran on both builds and the command was killed immediately. Read's offset: -3 was denied on every build, and the error quotes the schema minimum of 0.
Footnotes
-
Claude Code changelog, Anthropic, version 2.1.300, published to npm October 14, 2026. Source for the
updatedInputvalidation entry quoted in the introduction. ↩ ↩2 -
Hooks reference, Claude Code documentation, Anthropic, read October 15, 2026 and rechecked October 21, 2026. Source for where
updatedInputgoes, what it replaces, and the PermissionRequest placement. ↩ ↩2 ↩3 ↩4 -
PreToolUse hook updatedInput replaces entire tool_input instead of merging, silently drops unspecified fields, issue 27034 in the anthropics/claude-code repository as mirrored on claudeissues.com, opened February 20, 2026 and closed as a duplicate February 23, 2026. The quote is the reporter's own claim. ↩
-
Why updatedInput in a PreToolUse Hook Doesn't Rewrite the Command, mer.vin, September 25, 2026. By Mervin Praison. Reports 10 sessions per placement on Claude Code 2.1.282 only, and the debug-log line about unrecognized keys. ↩ ↩2
-
@anthropic-ai/claude-code on npm, npm registry publish times and dist-tags queried with
npm view, October 21, 2026. Source for the 2.1.299, 2.1.300 and 2.1.306 publish dates. ↩ -
Claude Code changelog, Anthropic, versions 2.1.301 to 2.1.306, read October 21, 2026. The sections I read did not mention
updatedInput. ↩
Free newsletter
Stay on the Nerd Track
Two emails a week: a Monday deep dive on one topic and a Thursday digest of courses, guides, posts, podcasts and jobs.