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

Open 💬 3 comments Opened Jul 13, 2026 by Zeitcatcher

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 ↗

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