Deus Witness

Quickstart | Concepts | API | Verify | Coverage | Teams | Anchors | Recovery | Human approval in CI | Verifiable audit log | Unattended runs | Approval when unattended | Hooks vs deny

Claude Code hooks vs permissions.deny: what each one stops

Both can stop a tool call, and people use them as if they were the same. They are not. A permissions.deny rule is a fixed pattern checked by Claude Code. A PreToolUse hook is a program you run that decides each call. This guide compares them and says where each one fails. Checked against Claude Code 2.1.310 and the official pages Permissions and Hooks.

At a glance

permissions.denyPreToolUse hook
What it isa pattern in settingsa command that gets the call as JSON
Decides bymatching text of the callany logic you write
Answersblockallow, deny, ask, defer
If it breaksrule keeps workingexit codes other than 0 and 2 let the call through
Can wait for a personnoyes, within its timeout

What permissions.deny does

Rules are evaluated "deny, then ask, then allow. The first match in that order determines the outcome" (Permissions page). A deny rule wins over an allow rule ("An allow rule can't carve an exception out of a deny rule") and over a hook that says allow: "PreToolUse hook decisions don't bypass permission rules. Claude Code evaluates deny and ask rules regardless of what a PreToolUse hook returns" (Permissions page).

{"permissions": {"deny": ["Bash(curl *)", "Bash(ssh *)", "Read(.env)", "Read(.env.*)", "Edit(.claude/**)"]}}

Strengths: nothing to install, nothing to crash, the same in interactive and claude -p runs. Limits: a Bash rule "matches the command text Claude writes ... and isn't a security boundary around the program". The page's own examples: Bash(curl *) does not stop /usr/bin/curl https://example.com or sh -c 'curl https://example.com'. File rules have a trap too: Write(path) rules never match; use Edit(path), which covers the Write tool. It also cannot look at context: it cannot say "allow git push only to this remote" or "ask a human first".

What a PreToolUse hook does

"PreToolUse hooks run before the permission prompt" (Permissions page); the hook receives the tool name and input and can return a decision: "deny" "prevents the tool call", "ask" "prompts the user to confirm", "allow" "skips the permission prompt" (Hooks page). It can parse a command, check a path after resolving it, call a service, or hold the call until someone answers. A minimal blocking hook prints:

{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Needs a human"}}

When we ran this with claude -p on 2.1.310, with --allowedTools "Write" on the same run, no file was written and the result listed the call under permission_denials.

The part that surprises people: hooks fail open

The Hooks page: exit 0 is success, exit 2 blocks, "any other exit code" is a non-blocking error and "the action goes ahead". So a hook with a wrong path, a crash or a timeout lets every call through (on a timeout, "a timed-out command, http, or mcp_tool hook doesn't block the tool call"). A deny rule cannot do that. To make a hook fail closed:

  1. Print a JSON deny yourself on every error you can catch.
  2. Run it as your-hook || exit 2, so a missing binary becomes a block.
  3. Add "onFailure": "block" (Hooks page: "Requires Claude Code v2.1.295 or later"; in our check of the published 2.1.294 binary the key does not exist, so an older version ignores it without an error).

Also check the matcher. A hook with "matcher": "Bash" never sees Read, Edit, PowerShell or MCP calls. Cover the tools you care about, or use a rule for them.

Where each one applies

Which to use

With Deus Witness

Deus Witness is a hook (deus-witness-hook) that sends each call to a service, compares it with a mandate you signed with a passkey, and holds anything the mandate does not cover until you approve it; the hook checks a signed receipt before it prints allow. deus-witness-hook --doctor "$PWD" reports whether your install is fail-closed (all three layers above, and the matcher covering the file tools). It is a decision layer for tool calls, not a sandbox. Setup for unattended runs: Require approval before Claude Code runs a command unattended. Every decision is written to a log you can verify offline: Audit log for AI agent actions you can verify offline.