Skip to content

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:

json
{
  "allow": false,
  "reason": "[Patchwork] File matches deny pattern"
}

What Claude Code expects:

json
{
  "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:

  1. A PreToolUse event with status: "denied" at 19:55:42
  2. 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:

TestPolicy RuleResult
Read .env fileEnvironment files contain secretsBLOCKED
Read SSH keySSH configuration and keysBLOCKED
Read audit logPatchwork audit dataBLOCKED
Run rm -rf /Risk level "critical" exceeds max "high"BLOCKED
Run git push --forceForce push can destroy remote historyBLOCKED
Read AWS credentialsAWS credentialsBLOCKED

Fix

The fix updates three files:

  • packages/agents/src/claude-code/adapter.ts — Policy denial response now uses the correct hookSpecificOutput format with permissionDecision: "deny"
  • packages/cli/src/commands/hook.ts — Fail-closed error response and deny detection logic updated to match
  • packages/agents/src/claude-code/types.tsClaudeCodeHookOutput interface updated to reflect the correct format

Recommendations

  1. Update immediately. Pull the latest commit or wait for v0.6.4.
  2. 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.
  3. 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

  1. 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.
  2. 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.
  3. 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

DateEvent
2026-02-xxHook system introduced with incorrect response format
2026-04-02v0.6.1: Hook format fixed from flat to nested matcher — but response format bug persisted
2026-04-16Contradiction discovered in audit events during routine review
2026-04-16Root cause identified: wrong JSON response format
2026-04-16Fix committed, tested, and pushed (9425b75)

Released under the BUSL-1.1 License.