Fail closed, not open

You'll finish with: Hooks that block when they break instead of waving things through.

The mistake almost everyone makes, and it's invisible.

You write a hook that depends on a tool being installed. On your machine it is. On a new laptop it isn't. The hook can't run, exits quietly, and allows everything. You believe you're protected. You're not.

When the hook itself breaks

Fails open
exit 0
allows everything, silently
Fails closed
exit 2
blocks and tells you

A guardrail that fails open is worse than no guardrail — because you stop watching. No guardrail at least keeps you careful.

Only exit 2 blocks. Nothing else does.

This is the part that catches people out, and it is the whole chapter in one sentence: a PreToolUse hook only stops the action if it exits with code 2. Anthropic's docs are explicit that exit 1 — the ordinary Unix "something went wrong" code — is treated as a non-blocking error and the action goes ahead anyway.

So a hook that crashes, or hits a missing command, or returns any failure code that isn't 2, has not protected you. It has logged a complaint while the thing happened.

Plain English

Your hook has to deliberately say "no" with exit 2. Merely failing is read as "this hook had a bad day, carry on". If you write your own hooks later and remember one thing from this course, make it this.

How the scripts here handle it

Both hooks try three interpreters in turn, and only if none of them can read the input do they give up — with exit 2, and a message saying why. They never fail silently and they never fail open.

if [ "$PARSED" -ne 1 ]; then
  echo "Blocked: this hook could not read its input, so it cannot tell whether" >&2
  echo "the action is safe. It needs a working python3 (or python) on PATH." >&2
  exit 2
fi

That block is the difference between a lock and a lock-shaped ornament.

Test the failure, not just the success

Anyone can check a hook blocks what it should. Almost nobody checks what happens when the hook itself is broken — which is the case that actually bites.

Do this now
  1. Temporarily rename your hook: mv protect-paths.sh protect-paths.bak
  2. Ask Claude to edit any file. It should still work — a missing hook isn't the same as a broken one.
  3. Rename it back.
  4. Now simulate the real failure. Open the hook and change the interpreter line for PY in python3 python py; do to for PY in nope; do. That's exactly what a machine without Python looks like to your hook.
  5. Ask Claude to edit any file. It should refuse, and print the "could not read its input" message.
  6. Change the line back to for PY in python3 python py; do and confirm normal edits work again.
How to know it worked

At step 5 the edit is refused and you can see why — the block message names the missing interpreter. At step 6 editing works normally again. That's a hook that fails closed and tells you, which is the entire point.

If step 5 lets the edit through, your hook is failing open. The usual cause is a guard that treats any non-zero exit as a block — remember only exit 2 stops anything, so a script that ends in exit 1, or crashes, waves the work through. Compare your file against the one above. Don't try to test this by putting exit 9 at the top of the hook: 9 isn't 2, so it won't block, and you'll wrongly conclude your setup is broken.

That's the course

Two locks on the four limits, both failing closed, both tested against the case that actually matters. Next, most people connect real tools — that's Connectors.

All courses · Start here · Privacy