if field with PowerShell(<specific>) patterns silently never matches; only PowerShell(*) works

Resolved 💬 3 comments Opened May 8, 2026 by sm-ep Closed May 9, 2026

Summary

When CLAUDE_CODE_USE_POWERSHELL_TOOL=1 is set, hook if-clauses of the form PowerShell(<pattern>) silently fail to match the PowerShell tool's actual tool_input.command for any non-trivial pattern. The universal-wildcard form PowerShell(*) matches; every other pattern does not, including patterns that literally match the captured tool_input.command string.

The identical Bash(<pattern>) form matches tool_input.command correctly.

This silently disables any hook gated on if: "PowerShell(<command-pattern>)", including realistic uses like PowerShell(gh pr create*) and PowerShell(git push*).

Environment

  • Claude Code 2.1.132 (native Windows install, not WSL)
  • Windows 11 Enterprise 10.0.22631
  • PowerShell 7.x
  • Relevant CLAUDE_CODE_* env (verbatim from the repro session):
  • CLAUDE_CODE_USE_POWERSHELL_TOOL=1 (gates the bug)
  • CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 (likely irrelevant; included pre-emptively)
  • CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 (likely irrelevant; included pre-emptively)
  • CLAUDE_CODE_ENTRYPOINT=cli

Repro

.claude/settings.local.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "PowerShell",
        "hooks": [
          {
            "type": "command",
            "shell": "powershell",
            "command": "Add-Content -Path \"$env:TEMP\\hook-wild.log\" -Value WILD; exit 0",
            "if": "PowerShell(*)",
            "timeout": 5
          },
          {
            "type": "command",
            "shell": "powershell",
            "command": "Add-Content -Path \"$env:TEMP\\hook-spec.log\" -Value SPEC; exit 0",
            "if": "PowerShell(Get-Date*)",
            "timeout": 5
          }
        ]
      }
    ]
  }
}

Then ask Claude to run Get-Date via the PowerShell tool and inspect both logs.

Expected

hook-wild.log and hook-spec.log both contain one entry. Both if rules are valid permission-rule-DSL strings; both should match the literal command Get-Date.

Actual

hook-wild.log contains one entry. hook-spec.log does not exist — the if: "PowerShell(Get-Date*)" rule never matches.

Additional confirmation: if: "PowerShell(*Get-Date*)", PowerShell(Power*), and PowerShell(PowerShell) likewise never match. The actual tool_input captured from stdin is {"command":"Get-Date","description":"..."} — the parser has access to the Get-Date string but does not match against it.

Likely root cause

The PowerShell(...) prefix is recognized at the parser level — proven by the fact that PowerShell(*) matches at all. The break is downstream, in the Bash-vs-PowerShell dispatch in the if-evaluator's command-extraction step: Bash has a working subcommand parser that returns tool_input.command; PowerShell appears to have none, an empty stub, or one that returns an empty string. The body glob is then matched against that empty result, so only * (and other patterns matching the empty string) ever fire.

Workaround

Drop the if clause and gate on the matcher alone, then disambiguate by inspecting tool_input.command inside the hook body. Two equivalent shapes:

PowerShell-inline:

{
  "matcher": "PowerShell",
  "hooks": [
    {
      "type": "command",
      "shell": "powershell",
      "command": "$j = [Console]::In.ReadToEnd() | ConvertFrom-Json; if ($j.tool_input.command -like 'git push*') { ...action... }; exit 0"
    }
  ]
}

Python script (sidesteps the bash-vs-pwsh shell-of-choice question — useful when a Bash twin entry runs the same logic and you want one script):

#!/usr/bin/env python3
import json, re, sys
data = json.load(sys.stdin)
cmd = data.get("tool_input", {}).get("command", "")
# Replace with whatever pattern PowerShell(<x>) was meant to express
if not re.match(r"^(gh|git)\b", cmd):
    sys.exit(0)
# ... your guard logic here ...

Either is less declarative than the if clause, but functional until the parser is fixed.

Related

#42318[DOCS] PowerShell docs omit key permission-check behavior. Same surface area (PowerShell + permissions/hooks DSL); this issue is the runtime-bug counterpart to that docs gap.

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗