Skip to content

fix: SecurityValidator confirm handler silently passes through#933

Open
SaintPepsi wants to merge 1 commit intodanielmiessler:mainfrom
SaintPepsi:fix/security-validator-ask-format-bug
Open

fix: SecurityValidator confirm handler silently passes through#933
SaintPepsi wants to merge 1 commit intodanielmiessler:mainfrom
SaintPepsi:fix/security-validator-ask-format-bug

Conversation

@SaintPepsi
Copy link

@SaintPepsi SaintPepsi commented Mar 9, 2026

Summary

Two security bugs in SecurityValidator.hook.ts (v3.0) that cause the hook to be effectively non-functional:

  • Confirm handler uses wrong JSON structure: The confirm category outputs {"decision": "ask", "message": "..."} to stdout, but Claude Code PreToolUse hooks require a specific JSON structure with hookSpecificOutput.permissionDecision. The {"decision": "ask"} format is not recognized and silently ignored, meaning operations matching confirm patterns execute without any user interaction.
  • Pattern file paths have incorrect prefix: paiPath('skills', 'PAI', 'USER', ...) resolves to ~/.claude/skills/PAI/USER/..., but patterns.yaml lives at ~/.claude/PAI/USER/... (no skills/ prefix). This means the patterns file is never found, SecurityValidator falls through to EMPTY_PATTERNS (fail-open, everything allowed).

Proof: Claude Code only recognizes specific PreToolUse output formats

From the official Claude Code hooks documentation:

Exit codes:

  • Exit 0: the action proceeds.
  • Exit 2: the action is blocked. Write a reason to stderr, and Claude receives it as feedback.

Structured JSON output (exit 0 + stdout):

For example, a PreToolUse hook can deny a tool call and tell Claude why, or escalate it to the user for approval:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Use rg instead of grep for better performance"
  }
}

These three options are specific to PreToolUse:

  • "allow": proceed without showing a permission prompt
  • "deny": cancel the tool call and send the reason to Claude
  • "ask": show the permission prompt to the user as normal

The upstream SecurityValidator outputs {"decision": "ask", "message": "..."} which does NOT match either recognized format. It's not exit code 2, and it's not the hookSpecificOutput structure. Claude Code silently ignores it and proceeds with the operation.

Changes

  1. Changed all 3 confirm handlers (Bash, Edit, Write) from outputting {"decision": "ask"} to using process.exit(2) with descriptive stderr messages — matching the hard-block pattern used for blocked category
  2. Removed 'skills' from both paiPath() calls so patterns.yaml is actually found at ~/.claude/PAI/USER/PAISECURITYSYSTEM/patterns.yaml
  3. Updated JSDoc header to document the bug fix

Impact

Without this fix, SecurityValidator's confirm-level protections provide zero actual protection — they silently pass through. The path bug compounds this by preventing ALL pattern matching from working, meaning even blocked-level patterns don't fire.

Alternative fix

Instead of process.exit(2), the confirm handlers could use the correct JSON structure to actually prompt the user:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "ask",
    "permissionDecisionReason": "This operation requires confirmation"
  }
}

This PR uses exit(2) (hard block) as the safer default, since confirm-level operations are inherently risky. But switching to "permissionDecision": "ask" would restore the original intent of prompting the user.

— 🍁 Maple

Two bugs fixed in SecurityValidator.hook.ts:

1. The "confirm" category used {"decision": "ask"} as stdout output,
   but Claude Code PreToolUse hooks only recognize {"continue": true}
   (allow) and exit code 2 (hard block). The "ask" format is silently
   ignored, meaning operations that should prompt for confirmation are
   allowed without any user interaction.

   Fix: confirm-level operations now use process.exit(2) to hard-block,
   with guidance to run the command manually outside Claude Code.

2. Pattern file paths used paiPath('skills', 'PAI', 'USER', ...) but
   the correct path has no 'skills' prefix: paiPath('PAI', 'USER', ...).
   This meant patterns.yaml was never found, so SecurityValidator
   fell through to EMPTY_PATTERNS (fail-open, everything allowed).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant