cd /news/ai-agents/fail-open-is-the-default-failure-mod… · home topics ai-agents article
[ARTICLE · art-124093] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=· neutral

Fail-open is the default failure mode of agent hooks

A developer's analysis of agent-CLI hook scripts reveals a common design flaw: they fail open, allowing unsafe operations when the hook crashes or returns no decision. The author of Handrail, a free hook pack for Claude Code, demonstrates a structural fix using a trap and default-deny logic, and notes that Handrail's CI tests enforce fail-closed behavior.

by read4 min views2 publishedSep 9, 2026

I'm the author of Handrail, a free, MIT-licensed hook pack for Claude Code and other

agent-CLI hook systems. Handrail works with Claude Code and other agent CLIs in plain

text only; it is not affiliated with, endorsed by, or a product of Anthropic. This post

is about one specific design bug I keep finding in hook scripts, including early

drafts of my own: they fail open.

An agent-CLI hook is a small program the harness calls before (or after) a tool call —

a shell command, a file write, a publish step — and asks, in effect, "should this be

allowed?" The hook's job is to answer allow, ask, or deny. The interesting question

isn't what the hook does when it works. It's what the harness does when the hook

doesn't answer at all.

Malformed JSON on stdin. An unhandled exception three lines into the script. A

timeout because the hook shelled out to something slow. A config file that doesn't

parse. In each of these cases, the hook process either exits with no usable decision,

or crashes before it prints one. What happens next depends entirely on what the

calling harness does with a hook that didn't answer — and a lot of hook scripts

never think about that side of the contract, because the code path for "I don't know,

so deny" is extra code nobody wrote until something forced the question. Independent

write-ups on this exact gap describe it as a live, common problem across shared hook

scripts, not a hypothetical (dev.to/redpa, "Your Claude Code hooks probably fail open —

here's why that's dangerous," accessed 2026-09-08).

The failure mode matters because of when it fires: exactly when the hook is under

the most stress — weird input, a broken environment, a partial config — which

correlates with exactly the moments a guardrail is most needed.

The fix isn't clever. It's structural:

Here's a minimal skeleton showing the shape (bash, illustrative — trimmed for a blog

post, not a drop-in hook):

#!/usr/bin/env bash
set -euo pipefail

trap 'echo "{\"decision\":\"deny\",\"reason\":\"hook error\"}"; exit 1' ERR

input="$(cat)" || { echo '{"decision":"deny","reason":"no input"}'; exit 1; }

command="$(jq -er '.tool_input.command // empty' <<<"$input" 2>/dev/null)" \
  || { echo '{"decision":"deny","reason":"unparsable input"}'; exit 1; }

[ -n "$command" ] || { echo '{"decision":"deny","reason":"empty command"}'; exit 1; }

if [[ "$command" =~ ^(ls|pwd|git\ status)$ ]]; then
  echo '{"decision":"allow"}'
  exit 0
fi

echo '{"decision":"deny","reason":"not on allow-list"}'

The load-bearing lines are the trap and the fact that the script only ever prints

allow from one narrow branch. Delete the allow-list entirely and the script is still

safe — it just asks or denies everything. Delete the trap, or let a parse error exit

before printing anything, and you're back to fail-open, silently.

A design principle that isn't tested is a design principle that regresses. Handrail's

CI spawns every shipped hook against a fixture set that includes malformed JSON, empty

stdin, and deliberately ambiguous config, and asserts the decision is deny in every

case — including when the hook process itself throws an uncaught exception. Any

fixture that returns allow fails the build. A second, related property is checked the

same way: nothing Handrail ships can widen or bypass an existing permission prompt or

default — only narrow it. That "only-tightens" property is a fixture-diff test in CI

too, not just a claim in a README.

Handrail is a defence-in-depth layer — it reduces risk but does not eliminate it, is

not a security audit or certification, and does not replace backups, code review, or

your own judgment. It only covers the rule categories it ships (six today: destructive

shell, git force-push/reset, secret paths and credential-shaped content,

prod-environment commands, package/deploy publishing, and remote code piped to a

shell); anything outside that surface is unguarded unless you write your own rule.

The free repo has the fail-closed harness above, the fixture suite, an installer that

merges into .claude/settings.json with a backup, and the six rules described above.

MIT, no signup: https://github.com/trimkeep/handrail-kit

There's also a paid early-access pack with a larger rule set, if you want more than

the free six rules cover: https://buy.polar.sh/polar_cl_xVkjNq9YQrEOJd25FaLVYBeXLqfgk4OU57LQZ4au38s

── more in #ai-agents 4 stories · sorted by recency
── more on @handrail 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/fail-open-is-the-def…] indexed:0 read:4min 2026-09-09 ·