if field with PowerShell(<specific>) patterns silently never matches; only PowerShell(*) works
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.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗