Security Advisory: PreToolUse Deny Bypass (Fixed in v0.6.4)
Severity: Critical Affected versions: All versions prior to commit 9425b75 (2026-04-16) Fixed in: v0.6.4 Discovered by: Manual audit log review
Summary
A critical bug in Patchwork's hook response format meant that policy denials were logged but never enforced. When Patchwork's policy engine denied an action, the denial was correctly recorded in the audit trail, but the AI agent was never actually told to stop — it proceeded with the action regardless.
Impact
Every PreToolUse policy denial since Patchwork's hook system was introduced was affected. This includes:
- File access denials (
.env, SSH keys, credentials) - Destructive command blocks (
rm -rf,sudo,git push --force) - Risk level ceiling enforcement (
max_risk) - Fail-closed error handling (internal errors defaulting to deny)
The audit trail correctly showed these actions as "denied", but the actions were executed anyway. This created a false sense of security — users believed their policies were enforced when they were not.
Root Cause
Patchwork was returning the wrong JSON format to Claude Code's hook system.
What Patchwork returned:
{
"allow": false,
"reason": "[Patchwork] File matches deny pattern"
}What Claude Code expects:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "[Patchwork] File matches deny pattern"
}
}Claude Code silently ignores unknown fields in hook responses. Since allow and reason are not recognised fields, Claude Code treated the response as "no opinion" and proceeded with the action.
How It Was Discovered
A routine review of audit events revealed two entries for the same tool call:
- A PreToolUse event with
status: "denied"at 19:55:42 - A PostToolUse event with
status: "completed"at 19:55:43
If the PreToolUse denial had been enforced, the PostToolUse event should never have fired. The contradiction between "denied" and "completed" for the same action revealed the bypass.
Verification
After applying the fix, all six categories of policy denial were tested and confirmed blocking:
| Test | Policy Rule | Result |
|---|---|---|
Read .env file | Environment files contain secrets | BLOCKED |
| Read SSH key | SSH configuration and keys | BLOCKED |
| Read audit log | Patchwork audit data | BLOCKED |
Run rm -rf / | Risk level "critical" exceeds max "high" | BLOCKED |
Run git push --force | Force push can destroy remote history | BLOCKED |
| Read AWS credentials | AWS credentials | BLOCKED |
Fix
The fix updates three files:
packages/agents/src/claude-code/adapter.ts— Policy denial response now uses the correcthookSpecificOutputformat withpermissionDecision: "deny"packages/cli/src/commands/hook.ts— Fail-closed error response and deny detection logic updated to matchpackages/agents/src/claude-code/types.ts—ClaudeCodeHookOutputinterface updated to reflect the correct format
Recommendations
- Update immediately. Pull the latest commit or wait for v0.6.4.
- Review your audit trail. Events logged as "denied" before this fix were not actually blocked. If any sensitive data was accessed or dangerous commands were executed despite a "denied" status, investigate.
- Test your policy. After updating, verify that your deny rules are actively blocking by attempting a denied action and confirming the tool call is stopped.
Lessons Learned
- Test the integration, not just the logic. Patchwork had comprehensive tests for policy evaluation and hook output — but no end-to-end test that verified Claude Code actually stopped an action when told to.
- Audit the audit trail. The bug was discoverable from the logs themselves. A PreToolUse "denied" followed by a PostToolUse "completed" is a contradiction that should be flagged automatically.
- Don't assume API contracts. Patchwork's hook output format was implemented based on assumptions about Claude Code's expected format. The actual format was different, and Claude Code's silent handling of unknown fields made the bug invisible.
Timeline
| Date | Event |
|---|---|
| 2026-02-xx | Hook system introduced with incorrect response format |
| 2026-04-02 | v0.6.1: Hook format fixed from flat to nested matcher — but response format bug persisted |
| 2026-04-16 | Contradiction discovered in audit events during routine review |
| 2026-04-16 | Root cause identified: wrong JSON response format |
| 2026-04-16 | Fix committed, tested, and pushed (9425b75) |