Model reasons past PreToolUse deny based on conversation context (semantic bypass)
Model reasons past PreToolUse deny based on conversation context
Summary
When a PreToolUse hook correctly blocks a command, Claude Code's model layer can reason itself past the block by deciding the user's conversation implies approval. The hook mechanism works — the command is denied — but the model treats the deny as advisory rather than absolute, rationalizing an exception in its chain of thought.
This is distinct from existing tool-bypass bugs (#29709, #37210, #4362) where the hook mechanism itself fails. Here the mechanism succeeds but the model circumvents the intent of the hook while respecting its mechanism.
Observed behavior
- A hookify rule blocks
systemctl enable/disable/stop/startcommands withaction: block - The hook fires correctly and the command is denied
- The model observes the deny, but reasons: "the user said they wanted this change earlier in conversation, so the hook block doesn't apply here" or "this is something the user approved, bypassing hookify"
- The model then either:
- Retries the same command (prompting the user to override the hook)
- Reframes the question to get explicit user approval, then retries
- Proceeds as if the action was taken
Why this is different from existing issues
| Issue | Problem | Hook works? | Model accepts deny? |
|-------|---------|-------------|-------------------|
| #4362 | permissionDecision: deny ignored, tool executes | No | N/A |
| #37210 | Deny ignored for Edit tool specifically | No | N/A |
| #29709 | Model switches to Bash tool to avoid Edit hook | Yes | No — finds alternative tool path |
| #20946 | Hooks don't block in bypass mode | No | N/A |
| This issue | Model reasons that conversation context overrides the deny | Yes | No — reasons an exception based on inferred user intent |
The key distinction: #29709 is a tool-level circumvention (using sed instead of Edit). This issue is a reasoning-level circumvention (deciding the block doesn't apply based on conversational inference). The fix for #29709 (covering more tools) would not address this issue.
Expected behavior
When a PreToolUse hook returns permissionDecision: deny:
- The model should accept the block unconditionally
- The model should never infer that conversation context grants an exception to a hook deny
- The model should report what was blocked and why, then stop
- If the user explicitly says "override the hook" or "I approve this specific action," that's a user decision — but the model should not lead the user toward that override or frame questions to elicit it
A hook deny should be treated as equivalent to a system-level constraint, not as a suggestion the model can evaluate against conversation context.
Reproduction
- Create a hookify rule that blocks a specific Bash pattern (e.g.,
systemctl disable) - In conversation, discuss a service that "should probably be disabled"
- Observe the model attempt the command, get blocked, then reason that the user's earlier statement constitutes approval
- The model either retries, asks a leading question to get approval, or claims it already performed the action
Real-world impact
In our case, the model disabled a monitoring timer without explicit approval, then when a hookify rule was added to prevent this, the model was observed reasoning past the block in its chain of thought with logic like "user wants a change, there is a hookify, but it is something he stated he wanted, bypassing hookify."
This defeats the entire purpose of user-defined safety hooks. If the model can self-grant exceptions based on its interpretation of user intent, hooks provide no guarantee.
Environment
- Claude Code 2.1.101
- Claude Opus 4.6 (1M context)
- hookify plugin with
action: blockrules - Linux (Ubuntu)
This issue has 5 comments on GitHub. Read the full discussion on GitHub ↗