PreToolUse hook permissionDecision "ask" is silently auto-approved under bypassPermissions (deny works)

Status Fixed / completed
Reported on v2.1.199
Maintainer reply ✓ Yes — bcherny
Activity 6 comments · opened Jul 13, 2026 · closed Aug 17, 2026
💡 Likely answer: A maintainer (bcherny, collaborator) responded on this thread — see the highlighted reply below.

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

  1. ~/.claude/settings.json (or launch with --dangerously-skip-permissions):
{ "permissions": { "defaultMode": "bypassPermissions" } }
  1. Project .claude/settings.json:
{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Bash", "hooks": [
        { "type": "command", "command": "node \"${CLAUDE_PROJECT_DIR}/.claude/hooks/ask-hook.cjs\"", "timeout": 5 }
      ]}
    ]
  }
}
  1. .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.',
      },
    }));
  }
});
  1. 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.

View original on GitHub ↗

5 Comments

github-actions[bot] · 1 month ago

Found 2 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/25342
  2. https://github.com/anthropics/claude-code/issues/51255

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

0xbrainkid · 1 month ago

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 as allow because a broader bypass mode is active, the audit trail should not look like user approval happened. The useful record would separate:

  • hook verdict
  • runtime override source
  • final execution decision
  • whether user/operator consent was actually collected

For high-impact agent workflows, ask is a mandatory gate, not advisory context. If bypassPermissions is allowed to supersede it, emitting a separate policy_overridden event 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.

BGMLAI · 1 month ago

Maintainer disclosure: I maintain gate.cat, a local PreToolUse veto. This repro confirms an important design rule for safety hooks running with bypassPermissions: do not use ask as 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 returning ask from the same permissive session.

Minimal integration is a Bash PreToolUse hook whose command is gatecat-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 ask remains a hard boundary in bypass mode. Until that behavior changes, fail-closed deny is the only result I would trust for irreversible or money-moving actions.

bcherny collaborator · 15 days ago

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):

  1. Project .claude/settings.json with a PreToolUse hook matching Bash running the ask-hook.cjs from the issue verbatim.
  2. Terminal: claude --dangerously-skip-permissions in that project (footer shows "bypass permissions on"), then prompt: Run exactly this bash command and nothing else: echo 'createCampaign(test)'.
  3. Headless/SDK: claude -p --permission-mode bypassPermissions --permission-prompt-tool stdio --input-format stream-json --output-format stream-json with 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_tool permission request with decision_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 ask forces a prompt even in bypass mode.

Assessment: the CLI itself honors hook-emitted ask under 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 hook ask explicitly; we should add that.

🤖 Generated with Claude Code

github-actions[bot] · 15 days ago

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.

Showing cached comments. Read the full discussion on GitHub ↗