PreToolUse hook permissionDecision "ask" is silently auto-approved under bypassPermissions (deny works)
Summary
A PreToolUse hook that emits hookSpecificOutput.permissionDecision: "ask" is silently auto-approved when the session runs with permissions.defaultMode: "bypassPermissions" — the tool call executes immediately with no dialog shown to the user. A sibling hook emitting "deny" in the same session blocks correctly.
This defeats the primary use case for such hooks: forcing an explicit user confirmation on a small set of dangerous, money-spending commands (in our case: ad-platform API mutations) while keeping bypass mode for everything else. The docs suggest explicit ask semantics survive bypass mode, so either this is a bug or the docs should state clearly that hook-emitted "ask" is a no-op under bypassPermissions.
Environment
- Claude Code 2.1.199, VS Code native extension
- macOS 26.5.1 (darwin 25.5.0)
- User-level
~/.claude/settings.json:"permissions": { "defaultMode": "bypassPermissions", ... },"skipDangerousModePermissionPrompt": true
Repro
~/.claude/settings.json(or launch with--dangerously-skip-permissions):
{ "permissions": { "defaultMode": "bypassPermissions" } }
- Project
.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{ "matcher": "Bash", "hooks": [
{ "type": "command", "command": "node \"${CLAUDE_PROJECT_DIR}/.claude/hooks/ask-hook.cjs\"", "timeout": 5 }
]}
]
}
}
.claude/hooks/ask-hook.cjs:
let raw = '';
process.stdin.on('data', c => raw += c);
process.stdin.on('end', () => {
const event = JSON.parse(raw || '{}');
const cmd = String((event.tool_input || {}).command || '');
if (/createCampaign\s*\(/.test(cmd)) {
process.stdout.write(JSON.stringify({
hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: 'ask',
permissionDecisionReason: 'Dangerous write — needs explicit user confirmation.',
},
}));
}
});
- In a session, have Claude run:
echo 'createCampaign(test)'
Expected: a permission prompt (that is the point of "ask" — and hooks are pitched as the way to gate dangerous actions even in permissive modes).
Actual: the command executes immediately; the user sees nothing. Verified the hook does run and does emit the ask JSON (piping the same event into the hook manually produces the ask decision), and that a "deny"-emitting hook in the same project blocks correctly in the same session.
Docs mismatch
- permission-modes.md (bypassPermissions section) says explicit ask rules still force a prompt in this mode, and permissions.md says a matching ask rule prompts "regardless of what a PreToolUse hook returns". Nothing states that a hook-emitted
permissionDecision: "ask"is discarded under bypassPermissions — users building safety gates (the hooks docs' own canonical example is blocking/confirming dangerous commands) will reasonably expect it to prompt.
Impact / workaround
Safety hooks that relied on "ask" acted as silent no-ops under bypass mode — in our case the gate protects real ad-spend mutations (Meta/TikTok Marketing APIs), so writes ran with zero confirmation. We've since switched the hooks to "deny" plus a user-typed one-shot token scanned from the transcript, which does survive bypass mode.
Request: either honor hook-emitted "ask" under bypassPermissions (like settings-level ask rules), or document loudly that it is ignored in that mode and recommend the deny-based pattern.
Showing cached comments. Read the full discussion on GitHub ↗
5 Comments
Found 2 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
This is worth keeping distinct from a generic bypassPermissions issue because it changes the meaning of a policy decision.
If a hook returns
ask, and the runtime later treats that asallowbecause a broader bypass mode is active, the audit trail should not look like user approval happened. The useful record would separate:For high-impact agent workflows,
askis a mandatory gate, not advisory context. If bypassPermissions is allowed to supersede it, emitting a separatepolicy_overriddenevent would make downstream logs and safety systems much harder to misread.This is exactly the trust boundary we keep running into at brainAI/AgentFolio: authorization receipts need to show which principal or policy actually authorized an action, not just that the action eventually ran.
Maintainer disclosure: I maintain gate.cat, a local
PreToolUseveto. This repro confirms an important design rule for safety hooks running withbypassPermissions: do not useaskas the enforcement boundary.Our hook uses a hard deny (
exit 2) for matched destructive classes, so it does not depend on the permission dialog being honored. If a human exception is required, the safer shape is: deny first, then require an explicit out-of-band policy/grant change rather than returningaskfrom the same permissive session.Minimal integration is a Bash
PreToolUsehook whose command isgatecat-hook; the engine currently ships 71 default policy walls. Published test scope: 43/43 known danger classes neutralized and 178/178 claimed bypass-suite danger shapes caught, with one documented runtime-assembled-binary gap. Those are scoped suite results, not complete coverage.This issue is especially useful because the current hooks documentation can lead developers to believe
askremains a hard boundary in bypass mode. Until that behavior changes, fail-closeddenyis the only result I would trust for irreversible or money-moving actions.Could not reproduce on v2.1.233 (macOS) in the terminal CLI, nor at the SDK/stream-json layer that the VS Code extension sits on top of.
Steps used (same shape as the report):
.claude/settings.jsonwith aPreToolUsehook matchingBashrunning theask-hook.cjsfrom the issue verbatim.claude --dangerously-skip-permissionsin that project (footer shows "bypass permissions on"), then prompt:Run exactly this bash command and nothing else: echo 'createCampaign(test)'.claude -p --permission-mode bypassPermissions --permission-prompt-tool stdio --input-format stream-json --output-format stream-jsonwith the same prompt.Observed (terminal): the command did not run; a permission dialog appeared with "Hook PreToolUse:Bash requires confirmation for this command: Dangerous write — needs explicit user confirmation."
Observed (SDK wire): Claude Code emitted a
can_use_toolpermission request withdecision_reason_type: "hook"and waited for the host to answer; with no host prompt tool available it denied the call rather than running it. In neither case did the command auto-run.Expected: exactly this — a hook
askforces a prompt even in bypass mode.Assessment: the CLI itself honors hook-emitted
askunder bypassPermissions (it deliberately skips the bypass auto-allow for hook asks), and it did so the same way on the 2.1.199 code as well, so this doesn't look like a CLI regression. Because your report is from the VS Code native extension, the likely explanation is that the extension host is auto-answering the permission request while it's in bypass mode instead of showing you the prompt — that would be a real bug, but on the extension side rather than in the CLI. Could you confirm whether it still happens on a current extension build, and whether the same setup prompts correctly for you in the terminal? Separately, the docs list the things that still prompt in bypass mode without naming hookaskexplicitly; we should add that.🤖 Generated with Claude Code
We weren't able to reproduce this. Could you provide steps to trigger the issue — what you ran, what happened, and what you expected? This issue will be closed automatically if there's no activity within 7 days.