Claude Code onFailure block: fail-closed hook for paid Sume tools

Claude Code 2.1.295 adds onFailure: block for command and HTTP hooks. Use it so a crashed spend guard stops paid Sume generate calls instead of allowing them.

4 min readSume
All posts

Set "onFailure": "block" on a PreToolUse command hook and Claude Code 2.1.295 (October 8, 2026) stops the tool call whenever that hook cannot start, times out, or exits with an unexpected code. Before this release a crashed guard simply let the call through. For paid Sume tools such as generate_video, that is the difference between a spend check that fails closed and one that silently disappears.

What changed in 2.1.295?

The changelog entry reads: added onFailure: "block" for command and HTTP hooks, so a hook that can't start, times out, or exits with an unexpected code blocks the action instead of letting it through. The hooks reference says the option needs v2.1.295 or later, defaults to continue, and that block is equivalent to exit code 2.

The old behavior matters because only exit code 2 was a blocking error. Any other non-zero exit was a non-blocking error: the action proceeded and you saw a hook error notice. A guard script with a typo, a missing interpreter, or a slow start therefore approved everything.

Which Sume calls deserve a fail-closed guard?

Sume's hosted MCP server at https://mcp.sume.com/mcp has read tools and paid or write tools. Its docs say OAuth mcp:read is read-only and mcp:write enables data-changing and paid tools, and that API-key calls need an idempotency_key for paid and write operations. The server's own run contract also asks for max_spend_usd when you want to cap a create. Those are the arguments worth checking before the call leaves your machine.

Claude Code names MCP tools mcp__<server>__<tool>. If you added the server as sume, the paid creators match mcp__sume__generate_image and mcp__sume__generate_video.

What does the hook look like?

Put this in .claude/settings.json. The 10-second timeout is well under the 600-second command default, and a timeout now blocks because of onFailure.

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "mcp__sume__(generate_image|generate_video)",
      "hooks": [{
        "type": "command",
        "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/sume_spend_guard.py\"",
        "timeout": 10,
        "onFailure": "block"
      }]
    }]
  }
}

The script reads the PreToolUse JSON from stdin, which carries the call under tool_input, and exits 2 with a reason on stderr when a field is missing. The except branch is deliberate: unreadable input also blocks.

import json, sys

try:
    call = json.load(sys.stdin)
    args = call.get("tool_input") or {}
except Exception:
    sys.exit(2)  # unreadable input: fail closed

missing = [k for k in ("idempotency_key", "max_spend_usd") if not args.get(k)]
if missing:
    print("Sume paid call blocked, missing: " + ", ".join(missing), file=sys.stderr)
    sys.exit(2)
sys.exit(0)

Which hook outcomes now block?

PreToolUse command hook outcomes, from the Claude Code hooks reference (read 2026-10-12)
Hook outcomeWithout onFailure blockWith onFailure: block
Exit 0Call proceedsCall proceeds
Exit 2Call blockedCall blocked
Other exit code (for example a Python traceback exits 1)Call proceeds, hook error noticeCall blocked
Cannot start or times outCall proceedsCall blocked

What should you still not rely on?

A hook is a local convenience, not a wallet. Sume's own admission and wallet controls decide whether a job is accepted, and a client-side check cannot raise or lower that. Keep idempotency_key unique per intended job, because a retry of the same key is how a repeated call avoids a second charge.

If you would rather be asked than blocked, an ask permission rule on the paid tools is the lighter option; the ask-rule post covers it, and the dry-run hook post shows a hook that previews cost first.

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume