[DOCS] Hook reference omits the PowerShell tool's tool_input schema, so a PreToolUse PowerShell hook fails open on a guessed field name

Status Fixed / completed
Reported on v2.1.220
Maintainer reply None cached
Activity 1 comment · opened Aug 3, 2026 · closed Aug 16, 2026

Documentation Type

Missing documentation (feature not documented)

Documentation Location

https://code.claude.com/docs/en/hooks

Section/Topic

PreToolUse hook input, tool_input fields per tool

Current Documentation

The tools reference is explicit that its table holds the exact strings used in hook matchers, and PowerShell is one of them:

PowerShell | Executes PowerShell commands natively. See PowerShell tool for availability

The hooks reference then documents tool_input for Bash:

{
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test",
    "description": "Run test suite",
    "timeout": 120000,
    "run_in_background": false
  }
}

There is no equivalent for PowerShell anywhere in the hooks docs.

What's Wrong or Missing?

A PreToolUse hook matching PowerShell has no documented way to find the command it is supposed to inspect. The field name is not in the hooks reference, the tools reference, or the settings docs.

I needed it to extend a PreToolUse security hook to Windows, and ended up reading strings out of the installed binary (2.1.220) to get it:

Executes a given PowerShell command with optional timeout. Working directory persists between commands; shell state (variables, functions) does not.
...
PowerShellTool: exec spawn failed:
Cannot destructure property 'command' from null or undefined value

So it is tool_input.command, same as Bash. That is a reasonable guess, but a guess is not a contract, and the cost of getting it wrong is asymmetric. A hook that reads the wrong key sees no command, follows the normal convention of deferring when it cannot parse its input, and emits nothing. Claude Code reads that as no opinion and runs the tool. The hook stays registered, /hooks still lists it, and it enforces nothing, with no error anywhere.

This lands hardest on Windows, and it does so without anyone changing configuration. Git for Windows is documented as optional, and without it there is no Bash tool at all, so every shell command goes through PowerShell. A hook author who matched Bash (following the documented example, which is the only one there is) has full coverage on macOS and Linux and zero shell coverage on that Windows machine.

#57137 (closed) is the same family: PowerShell(<specific>) permission patterns silently never matched, only PowerShell(*) worked. The PowerShell surface tends to fail quiet rather than loud, which is what makes the missing schema expensive.

Suggested Improvement

Any of these would resolve it, roughly in order of value:

  1. Add a PowerShell example next to the Bash one in the hooks input schema section, with the same field list (command, description, timeout, run_in_background) so the equivalence is stated rather than inferred.
  2. Add a line to the PowerShell tool section of the tools reference pointing at the hook input schema, since that is where someone configuring a matcher starts.
  3. Add a note to the hooks guide that a matcher intended to cover shell commands needs both Bash and PowerShell to be complete on Windows, and say why (Git for Windows is optional, and the PowerShell tool is rolling out where it is installed).

Point 3 is the one I would have most wanted. The schema gap cost me an afternoon; the matcher gap is the one that would have shipped a hook I believed was enforcing something.

Happy to send a docs PR for any of these if that is useful.

Impact

Medium - Makes feature difficult to understand

Additional Context

  • Version: 2.1.220, macOS (the PowerShell tool is opt-in here via CLAUDE_CODE_USE_POWERSHELL_TOOL=1 and PowerShell 7+, automatic on Windows without Git Bash).
  • Related: #57137 (closed), PowerShell-specific permission patterns silently not matching. #70696 is a separate hooks-reference gap.
  • Found while adding Windows coverage to a PreToolUse workspace guard, so the concrete need was "which key holds the command string", but the same gap applies to anyone writing audit or policy hooks.

View original on GitHub ↗

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