Skip to content

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

Description

@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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions