[BUG] PreToolUse `tool_input` differs from the transcript's `tool_use.input` when the tool schema strips a key (Agent `run_in_background` under the fork-subagent gate)
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet (closest: #91225, which is the fork gate overriding foreground; this report is about the hook/transcript input mismatch it causes)
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code (2.1.292, macOS)
What's Wrong?
The session transcript (~/.claude/projects/<proj>/<session>.jsonl) records the model's raw tool input, but PreToolUse hooks receive the schema-parsed input. When the schema drops or coerces a key, the two disagree, so a hook that cross-checks the call it is gating against the transcript (the documented transcript_path field) cannot match them.
Concrete case: in 2.1.292 the Agent schema omits run_in_background when epr() || MZ() holds, meaning background launch is restricted or the fork-subagent gate is on (MZ(), default ON). If the model still sends the key:
| Where | Agent input keys |
|---|---|
| Transcript tool_use.input | description, prompt, subagent_type, run_in_background (value: the string "false") |
| PreToolUse tool_input | description, prompt, subagent_type |
Observed in 7 of 7 such calls across two real sessionKind: "bg" sessions, and reproduced in nested --bg probe sessions running a logging PreToolUse hook. I have not captured the payload in a foreground session with only the fork gate on. The schema code path is the same one.
With CLAUDE_CODE_FORK_SUBAGENT=false set (settings env, after a restart) the key is in the schema, the call runs in the foreground, and the transcript records a real boolean false, matching the hook payload.
What Should Happen?
PreToolUse tool_input and the transcript's tool_use.input should describe the same call. Any of these would fix it:
- Write the parsed input (what actually runs) to the transcript, or record both.
- Give the hook the raw input too, e.g.
tool_input_raw. - Reject unknown keys with a validation error instead of stripping them silently. The model would then learn the parameter isn't there.
Steps to Reproduce
- Claude Code 2.1.292 in a session whose Agent schema omits
run_in_background. Observed in a--bgsession; per the schema code, the default fork-subagent gate should produce the same omission. - Register a PreToolUse hook on
Agentthat logstool_inputto a file and allows the call. - Ask Claude to call Agent with
run_in_background: false. It may emit the string"false". - Compare the hook's logged
tool_inputwith thattool_userecord in the transcript. The hook copy is missingrun_in_background; the transcript has it.
Impact
Third-party guards that bind a tool call to the transcript deny every such call with no way for the user to recover. gstack's /autoplan publication hook denied every reviewer dispatch across three attempts (garrytan/gstack#3062).
Related (separate bugs, not filed here)
- When the key is omitted in a
sessionKind: "bg"session, the turn'stool_userecord often isn't flushed to the transcript until the PreToolUse hook returns: 1 of 6 within 20 s in probes, versus about 110 ms when the key is present. So a hook that waits for the record can never see it. - #91225: the fork gate overriding
run_in_background: false(2.1.252).
Claude Code Version
2.1.292 (macOS, Ghostty terminal)