
Fixing Broken Guardrails with onFailure Block in Claude Code
Learn about Claude Code version 2.1.295 and the crucial new onFailure: "block" setting for command and HTTP hooks. We dive into how to prevent broken or missing guard scripts from failing open, plus key caveats to consider before updating your configuration.
Show Notes
- Releases · anthropics/claude-code - GitHub: https://github.com/anthropics/claude-code/releases
Chapter 1
onFailure block: Making Guard Hooks Fail Closed
Lachlan Reed
Here's a fun one to start the day. You write a hook to stop Claude Code running recursive force deletes, you point it at a script, and you've got a typo in the path. Nothing breaks. Nothing warns you loudly. The delete just runs. This is The Claude Code Changelog, brought to you by Jellypod AI, and today's the fix for exactly that.
James Turner
Version 2.1.295, released October 8th, adds a setting called onFailure, set to "block", for command and HTTP hooks. The changelog says a hook that can't start, times out, or exits with an unexpected code blocks the action instead of letting it through. Version 2.1.294 shipped earlier the same day with two hook fixes, and we'll get to one of those.
Lachlan Reed
So let's be clear on the old behaviour. For events that can block, like PreToolUse, UserPromptSubmit, Stop and PreCompact, only exit code 2 halts anything. Exit 1, which every Unix person reads as failure, is treated as a nonblocking error. Missing script, missing jq, crash, timeout, same story. Claude Code logs it and carries on. A guard that's broken looks identical to a guard that's quiet.
James Turner
And that was a real gap in the docs too. Anthropic's hooks reference said a timed out hook doesn't block the tool call, and told you not to count on a stalled hook as a gate. A write up from Mixed News on October 9th noted that the reference page didn't mention onFailure at all when they read it on the 8th. So if you go looking and can't find it in the docs, that's why. The release notes are the source of truth right now.
Lachlan Reed
Here's the config. In your settings dot json, under hooks, PreToolUse, matcher Bash, you've got a hook object. Type is command. Command is your project dir variable, then slash dot claude slash hooks slash guard dot sh. Then two extra fields: timeout set to 5, and onFailure set to "block".
James Turner
Developers Digest tested this on 2.1.295 with six runs of the same request, just running an echo. Script path doesn't exist, no onFailure: the echo ran. Same missing path with onFailure block: blocked. A script that sleeps thirty seconds with a three second timeout: runs without the setting, blocked with it. A script hitting a jq error and exiting 1: blocked. And a healthy guard with a safe command still let it through. So the setting changes what a broken hook does, not what a working one does.
Lachlan Reed
The messages are useful too. You get something like: PreToolUse Bash hook error, the script path, failed, blocking because onFailure is "block". For a timeout it says timed out, same ending. So Claude reports back why it stopped, and you're not left guessing.
James Turner
Now the caveats, and there are a few. First, it's strictly opt in. Nothing on Anthropic's pages says the default changed, so every hook you wrote last month still fails open until you add the line. Second, the timeout trap. The default timeout for command and HTTP hooks is 600 seconds. A hung guard on an unattended session sits there for ten minutes before it fails, open or closed. That's why the example has timeout 5. A shell guard needs a few seconds at most.
Lachlan Reed
Third, scope. It covers command and HTTP hooks only. Prompt, agent and MCP tool hooks aren't covered, and Developers Digest points out MCP tool hooks still give a nonblocking error if the server's disconnected. Fourth, it only matters on events where exit 2 already blocks. Slap it on a PostToolUse hook and, well, the tool's already done its thing.
James Turner
Fifth is one for CI people. Classmethod tested headless mode with the p flag, and the outer claude process still exited 0 even when the hook blocked the action. So don't rely on the shell's exit status. Check the output for the block message. And one more subtle point from the Developers Digest tests: if your script itself is naive, say jq is missing so a variable comes out empty and your regex never matches, the script exits 0. From Claude's side the hook succeeded, so onFailure can't help. Use set dash e, u, o pipefail and a trap that exits 2, if you want belt and braces.
Lachlan Reed
Smaller things in these releases. Quick fire. Version 2.1.294 fixed prompt and agent hooks written as instructions, like "Block commands that...", which were allowing what they should block. Worth rechecking if you use those. Terminals that support OSC 7501, the Program Status Protocol, can now show whether Claude is working, waiting on you, or done, handy across tabs. Headless mode now prints one line to stderr in a terminal saying what it's waiting for if it stays open after the last turn.
James Turner
The copy picker now copies quoted text without the greater than markers. There's a new environment variable, CLAUDE_CODE_RETRY_WATCHDOG_MAX_WAIT_MS, capping how long unattended retry mode waits out 429 and 529 errors. And requests on one million context models now retry without the beta flag when a gateway, Bedrock, Vertex or Foundry refuses it, instead of every request failing.
Lachlan Reed
So the takeaway: audit your hooks that exist to say no. Add onFailure block, add a short timeout, and run claude version to make sure you're on 2.1.295 or later.