PreToolUse "ask" permissionDecision (and permissions.ask list) not enforced — tools run without confirmation
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Summary
PreToolUse hook permissionDecision: "ask" (and baseline permissions.ask list entries) are not enforced — tools run immediately with no confirmation prompt, even when a hook explicitly returns an "ask" decision with a reason.
Environment
- Claude Code version: 2.1.215
- OS: Windows 11 Pro (10.0.26200)
- Shell tool used: PowerShell (Windows PowerShell 5.1 /
powershell.exe) - Permission mode:
default(confirmed via Shift+Tab indicator — not bypass/auto-accept) - Config:
~/.claude/settings.json,permissions.askincludesPowerShell,Edit,Write,NotebookEdit; aPreToolUsehook is registered on thePowerShellmatcher
Steps to reproduce
- Configure a
PreToolUsehook on thePowerShellmatcher that reads the tool-call JSON from stdin and, for commands matching a "dangerous" pattern (e.g.Remove-Item), returns:
``json``
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"..."}}
- Verify the hook script works correctly by piping a simulated tool-call payload into it directly (outside the harness) — confirm it returns the
askJSON above for aRemove-Itemcommand. - Through the actual Claude Code session, ask Claude to run a
Remove-Itemcommand via the PowerShell tool.
What Should Happen?
Expected behavior
The tool call should pause and prompt for explicit user confirmation before running, per the hook's permissionDecision: "ask" output (and/or because PowerShell is on the permissions.ask list).
Error Messages/Logs
## Actual behavior
The command executes immediately. No confirmation prompt is shown. This was verified by checking that the target file was actually deleted, with no pause in the conversation.
This is not limited to the hook — `Write` and `NotebookEdit` (also on the `permissions.ask` list, no hook attached) exhibited the same behavior: zero prompts for an entire session, for any tool on the `ask` list.
Steps to Reproduce
Investigation / ruled-out causes
- Hook content was not the issue. Initially the hook script itself was silently crashing (see below), but even after fixing that and confirming — via direct, out-of-harness invocation — that it returns the correct
askJSON for dangerous commands and no output (implicit allow) for safe ones, live tool calls through the harness still bypass confirmation entirely. - Not a permission-mode issue. Confirmed session was in
defaultmode via the Shift+Tab indicator, notbypassPermissionsoracceptEdits. - Not a
settings.local.jsonoverride. No local override file present. - Separately found and fixed a real hook-invocation bug that may be related or may be a distinct issue:
"type": "command"hooks are executed via/usr/bin/bash -c "<command>"regardless of host OS. On Windows, a hookcommandwritten as a PowerShell script (as shown in Claude Code's own Windows hook examples) is therefore fed to bash, which cannot parse PowerShell syntax, producing an immediate syntax error on every invocation. Worked around this by moving the hook logic into a standalone.ps1file and settingcommandtopowershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "<path to .ps1>". This fixed the hook crashing, but did not fix the underlying lack of enforcement described above — suggesting the enforcement gap is a separate, harness-level issue from the shell-invocation one.
Claude Model
Sonnet (default)
Is this a regression?
No, this never worked
Last Working Version
_No response_
Claude Code Version
2.1.215 (Claude Code)
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
PowerShell
Additional Information
Additional context
Given the shell-invocation bug above, it's plausible other Windows users configuring PowerShell-based safety hooks (e.g. following any PowerShell-syntax hook example) are silently getting non-functional hooks without realizing it, since the failure mode observed was: hook crashes silently → tool call proceeds anyway → no visible error to the user in some cases, a bash syntax error surfaced as a hook error in others (both observed across two sessions six days apart, same config, same commands).
3 Comments
Still present in 2.1.220, and it is not mode-specific.
This issue reports
permissionMode: default. I see identical behavior with"defaultMode": "auto"in ~/.claude/settings.json, in a fully interactivesession (not background, not non-interactive, not bypassPermissions — those
conditions are covered by #79501 / #80138 / #77212, this is none of them).
Isolation, same session / same hook / same JSON shape, one word changed:
permissionDecision: "deny" -> command blocked, reason surfaced. Correct.
permissionDecision: "ask" -> command runs, no prompt at all.
So the ask path is what's broken, not the rule syntax and not the hook
plumbing — deny travels the same route and is honored.
Also confirming the settings.json half on this build: 17 explicit ask rules,
all listed in /permissions under "will always ask for confirmation", none of
them ever prompts. Failure is silent — no error, no warning, no log entry.
Env: Claude Code 2.1.220, Windows 11 Pro 10.0.26200, PowerShell.
Filed via /feedback as 37b8c9d8-7ce8-450b-baac-ff5f7d65c0df.
Reproduced on Linux, so this is not Windows- or PowerShell-specific — the
platform:windowslabel looks too narrow.Environment
x86_64)/usr/bin/zshas the shellpermissionModedefaultandacceptEdits— same result in bothControls
Both directions of the control were run with no change other than the decision string, which rules out the hook mechanism, the JSON shape, exit codes, and settings loading:
PreToolUsehook on theBashmatcher returning``
json
``{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"test"}}
produces no prompt; the command runs. Changing
"ask"to"deny"in the same script blocks the call immediately and renderspermissionDecisionReason."ask": ["Bash(rm:*)"]and"deny": ["Bash(touch:*)"]in the same~/.claude/settings.json:touch /tmp/xis blocked,rm -f /tmp/xruns with no prompt. Both rules load together, so "settings were not picked up" cannot explain the difference.I confirmed the hook actually executes (rather than being skipped) by having the script append to a log file — the log grows on every matching call in both the
askanddenyvariants.Why this one is worth prioritising
The failure is silent and looks healthy from every angle a user can check:
/hookslists the hook as registered, the hook process runs, and the judge emits a correct decision — but nothing reaches the user. A guardrail built onasktherefore enforces nothing while appearing installed. That is strictly worse than having no guardrail, since there is no signal to discover it from.The documentation still recommends
askas the middle ground betweenallowanddeny(Configure permissions → Extend permissions with hooks: "The hook output can deny the tool call, force a prompt, or skip the prompt"). On this version that recommendation yields no protection on Linux either.Workaround for anyone landing here: use
deny, or enforce outside Claude Code (git hooks, filesystem permissions).Hit both halves of this on Linux with the Bash tool on 2.1.234, in an interactive session started as
claude --permission-mode auto: apermissions.askentry did not prompt, and aPreToolUsehook returningpermissionDecision: "ask"for the same command did not prompt either.denystill gates in the same session.Full write-up with the repro and the controls: #87639. The parts that may be useful here:
askat the moment it was ignored. Adate >>sentinel prefixed onto the hook command wrote its file, and piping the command through the hook script as stored insettings.jsonprintedpermissionDecision: "ask".claude -p --permission-mode autoon the same build, including a check where a user-scope ask rule beat a matching user-scope allow rule. So rule syntax, user-scope loading, and ask-over-allow precedence all behave; the failure showed up only in the interactive session.Also agreeing with the earlier comments that
platform:windowsis too narrow: Windows/PowerShell, WSL2/Bash, and now Fedora/Bash.