↓ Skip to main content
  1. Articles/

Claude Code Hooks Can Now Fail Closed: onFailure: block and What v2.1.293–296 Changed

·1182 words·6 mins·
Florent Clairambault
Author
Florent Clairambault
CTO & software engineer — writing daily about spec-driven development and agentic coding

Claude Code Hooks Can Now Fail Closed: onFailure: block and What v2.1.293–296 Changed

Hooks are the part of Claude Code where policy becomes code: a script that runs before a tool call and says yes or no. Until this week, they had a quiet design flaw that anyone running them in CI or at an organization level should know about. If the hook itself broke, the action went through.

Claude Code v2.1.295, released October 8, 2026, closes that gap. Together with the fixes in v2.1.294 and the follow-up in v2.1.296, it is the most security-relevant batch of hook changes since Mods shipped in v2.1.287. Everything below comes from the official changelog.

The change: fail-closed hooks
#

From the v2.1.295 notes:

Added onFailure: "block" for command and HTTP hooks: a hook that can’t start, times out, or exits with an unexpected code blocks the action instead of letting it through.

Read that as a list of three failure modes that used to be silent allow-by-default:

  1. Can’t start. The script was deleted, isn’t executable, or the interpreter is missing on that runner.
  2. Times out. The policy service behind an HTTP hook is slow or down.
  3. Unexpected exit code. The script crashed with a stack trace instead of returning a deliberate verdict.

Every one of those is exactly what happens on a bad day: a deploy removes a dependency, a policy endpoint is overloaded, a Python upgrade breaks an import. In a fail-open world, the day your guardrail breaks is the day you have none, and nobody is told.

A sketch of what opting in looks like in a settings file (the shape is illustrative; check the hooks docs for the exact schema in your version):

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/policy-check.sh",
            "timeout": 5,
            "onFailure": "block"
          }
        ]
      }
    ]
  }
}

With that in place, a policy-check.sh that vanishes or hangs for six seconds stops the Bash call rather than waving it through. That is the correct default for anything protecting production credentials, and the changelog entry reads as opt-in, so existing configurations keep their current behavior until you change them.

Two honest caveats
#

It is scoped to command and HTTP hooks. The changelog entry names those two types. prompt and agent hooks, which delegate the decision to a model, are not covered by this flag, and that matters for the next section.

Blocking has a cost. A fail-closed hook on a flaky HTTP endpoint turns a transient outage into a stalled agent. In an unattended run that means a stuck job, not a security incident, which is the right trade for sensitive actions and the wrong one for a hook that merely logs. Apply it selectively: block-on-failure for authorization, fail-open for telemetry.

The v2.1.294 fix you should read twice
#

The day before, v2.1.294 shipped this fix:

Fixed prompt and agent hooks written as instructions (such as “Block commands that…”) allowing what they should block.

Anthropic doesn’t elaborate on the mechanism in the changelog, so don’t read more into it than is there. But the implication is plain: if you wrote a prompt hook as an imperative instruction rather than as a question the model answers, it could approve the thing you told it to stop. The same release also changed how Stop and SubagentStop prompt hooks phrased as instructions (“Carry on if the build is broken”) are judged, so Claude is less likely to stop early.

Practical audit, today:

  • Grep your settings for "type": "prompt" and "type": "agent" hooks.
  • Find any that are phrased as commands to the model.
  • Upgrade to v2.1.294 or later, then re-test with a case that should be blocked. Don’t assume the old behavior caught it.

This is also a reminder of the general rule: a model-judged hook is a probabilistic control. Anything that must never happen belongs in a deterministic command hook, now with onFailure: "block", or in the sandbox and permission layers underneath.

MCP limits moved twice in two days
#

Two other entries are worth knowing if you run many MCP servers:

  • v2.1.295: MCP tool descriptions the model loads through tool search are now cut at 16,384 characters instead of 2,048.
  • v2.1.296 (Oct 9): the default limit on MCP tool descriptions sent up front, and on MCP server instructions, went from 2,048 to 4,096 characters.

These are two different limits, the first for descriptions loaded on demand and the second for what’s sent up front, and the changelog doesn’t say whether they are meant to converge. The security angle is the one I keep returning to: tool descriptions are text the model reads, which makes them an injection surface. A bigger window is more room for a legitimate, detailed description and also more room for a malicious one. If you consume third-party MCP servers, a higher cap is a reason to review what they actually send, not a reason to relax.

v2.1.295 also made claude.ai connectors negotiate MCP protocol version 2026-07-28 by default on installs that fetch no flags, with MCP_PROTOCOL_NEGOTIATION=legacy as the opt-out. If a connector misbehaves after upgrading, that’s the first knob to try.

The smaller items
#

  • OSC 7501 (Program Status Protocol). Terminals that implement it can show whether Claude Code is working, waiting on you, or done. It sounds cosmetic, but for people running several sessions in tabs it is the difference between polling and being told.
  • autoCompactWindow for subagents (v2.1.296). You can now set the auto-compaction window in subagent frontmatter and --agents definitions, which pairs naturally with cheap subagent models like the $0.10 Haiku 5.5.
  • Sonnet 5.5 cache reads at $0.10 per million tokens in /cost, the status line and --max-budget-usd (was $0.20), matching the price cut covered in the Haiku piece.
  • Subagents preload at most 32 skills from the skills field.
  • A [1m] fix. Every request no longer fails on a [1m] model when a gateway, Bedrock, Vertex or Foundry refuses the context-1m beta; Claude Code resends without it.

Why this matters for spec-driven teams
#

In a spec-driven workflow, the spec says what should happen and hooks enforce what must not. Enforcement that disappears when it errors is a suggestion. onFailure: "block" is Anthropic acknowledging that a guardrail needs a defined behavior for its own failure, which is basic safety engineering that most agent tooling still skips.

Contrast that with IDE-centered tools, where the safety model is largely a human watching a diff go by. That works until the human stops watching, and an autonomous agent is by definition running when you aren’t. Controls that fail closed, run without a person present, and are auditable are the ones that scale. The governance thread here is the same one running through Mods and the Sonnet 5.5 default release: Anthropic keeps adding policy surfaces, and the work for you is to actually configure them.

What to do this week
#

  1. Upgrade to v2.1.296.
  2. Add onFailure: "block" to every command or HTTP hook that guards credentials, deploys, or destructive commands.
  3. Re-test any instruction-phrased prompt/agent hook.
  4. Review the descriptions your MCP servers send, now that the limits are higher.

Sources

Related