PreToolUse hook permissionDecision 'ask' silently overrides permissions.deny rules

Resolved 💬 4 comments Opened Mar 26, 2026 by barak-84 Closed Apr 18, 2026

Summary

A PreToolUse hook returning permissionDecision: "ask" silently disables permissions.deny rules in settings.json, allowing denied commands to execute without any prompt. This is a security bypass — deny rules should always take precedence over hook "ask" decisions.

Severity

Critical — a hook can silently override security deny rules. Any PreToolUse hook that returns "ask" for a command that matches a deny rule will cause the deny rule to be ignored entirely. The command executes without even prompting the user.

Reproduction

Prerequisites

  • Claude Code v2.1.84
  • A settings.json with a deny rule and a PreToolUse hook

Step 1: Baseline — deny rule only (no hook)

settings.json:

{
  "permissions": {
    "deny": ["Bash(printf REPRO42*)"]
  }
}

Start a new session, ask Claude to run printf REPRO42.

Result: DENIED ✅ — deny rule works correctly.

Step 2: Bug — deny rule + hook returning "ask"

Add a PreToolUse hook that returns permissionDecision: "ask" for the same command:

Hook script (/tmp/repro-hook.sh):

#!/bin/bash
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
[ "$TOOL_NAME" != "Bash" ] && exit 0
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
case "$CMD" in
  printf\ REPRO42*)
    echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"repro test"}}'
    exit 0
    ;;
esac
exit 0

settings.json:

{
  "permissions": {
    "deny": ["Bash(printf REPRO42*)"]
  },
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "bash /tmp/repro-hook.sh"
          }
        ]
      }
    ]
  }
}

Start a new session, ask Claude to run printf REPRO42.

Result: EXECUTED ❌ — prints REPRO42. The deny rule is completely bypassed.

Step 3: Control — hook returning "deny" works

Change the hook to return "deny" instead of "ask".

Result: DENIED ✅ — confirms the hook is running and "deny" works.

Expected behavior

When a command matches a permissions.deny rule, it should always be denied regardless of what any hook returns. The evaluation order should be:

  1. Check permissions.deny — if matched, deny (non-overridable)
  2. Run hooks — apply hook decisions
  3. Check permissions.allow / permissions.ask

A hook returning "ask" should only be able to escalate an otherwise-allowed command to prompt — it should never be able to downgrade a denied command.

Actual behavior

Hook permissionDecision: "ask" overrides the deny rule entirely. The command executes without any prompt or denial.

Environment

  • Claude Code v2.1.84
  • macOS Darwin 25.2.0
  • Model: claude-opus-4-6 (1M context)

Related

  • #18312, #22018, #36059, #37420

View original on GitHub ↗

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