[BUG] PreToolUse hook is not invoked after a static ask rule for the same command pattern receives session-level approval
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
PreToolUse hook is not invoked after a static ask rule for the same command pattern receives session-level approval
Expected: Hook should always fire on PreToolUse regardless of static rule state — the hook's permissionDecision should be evaluated independently, especially for deny decisions which must not be suppressible by a prior session approval.
What Should Happen?
Expected: Hook should always fire on PreToolUse regardless of static rule state — the hook's permissionDecision should be evaluated independently, especially for deny decisions which must not be suppressible by a prior session approval.
Actual: Once Bash(docker --host:*) was session-approved via the ask prompt, subsequent docker --host ... rm and docker --host ... rmi commands bypassed the hook entirely (no log entries written, no deny enforced).
Why it matters: A deny from a hook should be unconditional — a session approval of a broad pattern like docker --host:* should not be able to whitelist destructive subcommands that the hook is explicitly denying.
Reproduction: Add a broad ask pattern + a PreToolUse hook that denies a subset of matching commands → approve the ask prompt once → subsequent deny-eligible commands run freely.
Essentially the json configs should be independent of hooks
Error Messages/Logs
Steps to Reproduce
Minimal repro for Claude Code hook bypass bug:
- Create a PreToolUse hook (~/.claude/hooks/guard.py):
#!/usr/bin/env python3
import json, sys, shlex
data = json.load(sys.stdin)
command = data.get('tool_input', {}).get('command', '').strip()
try:
tokens = shlex.split(command)
except ValueError:
sys.exit(0)
# Skip global flags to find real subcommand
i = 1
while i < len(tokens) and tokens[i].startswith('-'):
i += 2 if tokens[i] in ('--host', '-H') and i+1 < len(tokens) else 1
subcommand = tokens[i] if i < len(tokens) else None
if subcommand in ('rm', 'rmi'):
print(json.dumps({'hookSpecificOutput': {'hookEventName': 'PreToolUse', 'permissionDecision': 'deny', 'permissionDecisionReason': 'guard: destructive'}}))
sys.exit(0)
- Register it in ~/.claude/settings.json:
{
"hooks": {
"PreToolUse": [{"matcher": "Bash", "hooks": [{"type": "command", "command": "python3 ~/.claude/hooks/guard.py"}]}]
},
"permissions": {
"ask": ["Bash(docker --host:*)"],
"deny": []
}
}
- Reproduce:
# Step A — trigger the ask prompt and APPROVE it:
docker --host unix:///var/run/docker.sock ps
# Step B — now try a destructive command:
docker --host unix:///var/run/docker.sock rm <any-stopped-container>
Expected: Hook fires, deny blocks the command.
Actual: Hook is not invoked. Command reaches the Docker daemon.
Observable proof: Add open('/tmp/hook.log', 'a').write(command + '\n') to the hook — no log entry appears for step B.
Claude Model
Sonnet (default)
Is this a regression?
No, this never worked
Last Working Version
_No response_
Claude Code Version
2.1.87 (Claude Code)
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Terminal.app (macOS)
Additional Information
I think this is a design issue at the end of the day. The claude.md and json ask/deny/allow should not override an independent hook as that is a different layer of safety unless that specific hook is explicitly overriden -
Root cause: When a static ask rule gets session-approved, Claude Code bypasses the PreToolUse hook for all subsequent matching commands. The hook's deny output is never reached and so this is essentially a serious safety/security flaw in how this is processed.
This issue has 5 comments on GitHub. Read the full discussion on GitHub ↗