[FEATURE] PreToolUse hooks should override permission grants for defense-in-depth security
Preflight Checklist
- [x] I have searched existing requests and this feature hasn't been requested yet
- [x] This is a single feature request (not multiple features)
Problem Statement
Summary
When implementing defense-in-depth security using PreToolUse hooks, users need certain high-risk commands (like git push, git merge, rm -rf) to always require explicit confirmation, even when broad Bash permissions are granted. Currently, hooks cannot override existing permission grants, making defense-in-depth impossible.
The Dilemma
Users face a forced choice between productivity and security:
- Grant broad Bash permissions → Fast development, but hooks'
permissionDecision: "ask"or"deny"are ignored for allowed tools - 2. Keep Bash restricted → Hooks work, but constant permission prompts for every piped/chained command during development
Real-World Use Case
I implemented a PreToolUse hook (security-check.sh) that:
- Detects dangerous commands:
git push,git merge,git rebase,rm -rf - - Returns
{"permissionDecision": "ask", "reason": "Destructive git operation requires confirmation"}
Expected: Hook prompts user for confirmation before these specific commands
Actual: Hook is completely bypassed because Bash is in the allow list
Related Issues (This is a Longstanding Problem)
This issue has been reported multiple times and affects many users:
- #4669 -
permissionDecision: "deny"in PreToolUse hooks is ignored (closed due to inactivity, not resolved) - - #6527 -
asklist is ignored when "Bash" is in allow list - - - #13214 - PreToolUse hooks bypassed when permission rule matches
- - - - #13339 - VS Code Extension ignores Hook
permissionDecision: "ask" - - - - - #13744, #13756 - PreToolUse hooks with exit code 2 don't block tool execution
- - - - - - #15237 -
askpermission rules don't take precedence over allow - - - - - - - #18312 - PreToolUse hook permissionDecision ignored when tool in allow list
- - - - - - - - #12690 - Allow list overrides deny list (possible destructive behavior)
User @tomomonta's testing in #4669 confirmed ALL blocking mechanisms fail:
permissionDecision: "deny"- Tool executes anyway- 2.
permissionDecision: "ask"- No user prompt shown, tool executes - 3.
"continue": false- Tool execution continues regardless - 4. Non-zero exit codes - Tool proceeds even with exit code 1 or 2
Proposed Solution
Core Request: Hooks Should Always Run First
PreToolUse hooks should execute before permission evaluation, and their decisions should be respected regardless of existing permission grants.
Proposed Implementation
Option A: Hook Priority Flag (Preferred)
Add a priority: "override" option to hook configuration:
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"priority": "override", // Hook runs and its decision is respected even if tool is allowed
"hooks": [{"type": "command", "command": "./security-check.sh"}]
}]
}
}
Option B: New Permission Decision Value
Add "force-ask" and "force-deny" that cannot be overridden:
{"permissionDecision": "force-ask", "reason": "Destructive operation requires confirmation"}
Option C: Fix Current Behavior
Make the documented behavior actually work - hooks should run before permission checks as the documentation states:
"When Claude Code makes a tool call, PreToolUse hooks run before the permission system runs, and the hook output can determine whether to approve or deny the tool call in place of the permission system."
Expected User Experience
- User grants broad Bash permission for productivity
- 2. User configures hook to intercept dangerous commands
- 3. Normal commands execute without prompts
- 4. Dangerous commands (git push, rm -rf, etc.) trigger user confirmation via the hook
- 5. User confirms or denies the specific operation
This enables defense-in-depth: multiple independent security layers where hooks act as a final safety check regardless of permission settings.
Alternative Solutions
Current Workarounds (All Inadequate)
1. Hard Block via Exit Code 2
- What it does: Completely blocks the command
- - Problem: Can't allow the operation after user confirmation - it's all-or-nothing
- - - Use case broken: Can't use Claude for legitimate git push operations
2. Remove Bash from Allow List
- What it does: Hooks work as intended
- - Problem: Constant permission prompts for every piped command, chained command, and multi-command during development
- - - Use case broken: Development velocity destroyed by prompt fatigue
3. Manual Command Execution
- What it does: Claude tells user to run dangerous commands manually
- - Problem: Breaks the agentic workflow entirely
- - - Use case broken: Defeats the purpose of AI-assisted development
4. Wrapper Scripts with Aliases
- What it does: User creates
gpushalias to rungit pushoutside Claude - - Problem: Doesn't integrate with Claude's workflow or permission system
- - - Use case broken: Manual intervention required for every deployment
Why This Matters
Enterprise and security-conscious users want to use Claude Code but need guardrails. The current permission system forces an all-or-nothing choice that doesn't reflect how security actually works in practice.
Defense-in-depth is a fundamental security principle: multiple independent layers of protection. Hooks were designed to be one of those layers, but they're currently ineffective when combined with practical permission settings.
Environment
- Claude Code: Latest version
- - Platform: macOS (but affects all platforms based on related issues)
- - - Configuration: Project-level
.claude/settings.json
Priority
Medium - Would be very helpful
Feature Category
Configuration and settings
Use Case Example
- I'm developing a multi-tenant SaaS application with Claude Code
- 2. I grant broad Bash permissions so Claude can run tests, linters, build commands without constant prompts
- 3. I create a PreToolUse hook that detects
git pushandgit mergecommands - 4. Hook returns
{"permissionDecision": "ask", "reason": "About to push to remote - please confirm"} - 5. Expected: Claude pauses and asks me to confirm before pushing
- 6. Actual: Hook is bypassed, push executes immediately without confirmation
- 7. This matters because an accidental push to production could deploy untested code
Additional Context
_No response_
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗