Claude Code hook: exit 2 or a JSON deny for a paid Sume call?

Exit code 2 blocks and cannot be overridden by JSON; exit 0 with permissionDecision deny carries a reason. A runnable Python guard for Sume's paid tools.

3 min readSume
All posts

Prefer exit 0 with a JSON permissionDecision of deny when you block a paid Sume call. The hooks reference says exit code 2 is a blocking error that no JSON can override, while exit 0 lets the JSON fields decide and carries a permissionDecisionReason that Claude can act on. Use exit 2 only for a hard stop.

Below is a guard for the paid tools that denies any call missing an idempotency_key, which Sume requires on write and paid tools anyway.

What each exit path does

Both come from the hooks reference.

Hook outcomes (read 2026-10-08)
ExitJSON honoredUse for
0YesDecisions with a reason, such as deny, ask, allow or defer
1 to 5 (non-blocking)If validErrors that should not stop the call
2No, cannot be overriddenHard blocking errors
OtherIf validTreated as non-blocking

The guard

Save as guard_sume.py and call it from a PreToolUse hook whose matcher lists the paid names exactly. It reads the hook input from stdin, so it runs as a normal script.

import json
import sys


def main() -> None:
    data = json.load(sys.stdin)
    args = data.get("tool_input") or {}
    if args.get("idempotency_key"):
        return
    print(json.dumps({
        "hookSpecificOutput": {
            "hookEventName": "PreToolUse",
            "permissionDecision": "deny",
            "permissionDecisionReason": (
                "Paid Sume tools need a stable idempotency_key. "
                "Retry with one."
            ),
        }
    }))


main()

Wire it up and test

Register it under hooks.PreToolUse with a command such as python3 guard_sume.py and the matcher mcp__sume__generate_image|mcp__sume__generate_video. Ask the model to render something and watch the reason text come back. Because the hook exits 0, Claude sees the reason and can retry with a key. Then try dry_run=true, which Sume documents as an admission and cost preview that does not submit a job.

  • A key that changes on every retry defeats deduplication; generate it once per intended job.
  • Add a second check for max_spend_usd if your policy needs a cap.
  • Run claude --debug-file hooks.log to see hook execution if a deny does not appear.

Choosing between deny, ask, defer and allow

The reference lists four PreToolUse decisions: deny, allow, ask and defer. For a paid Sume call, a deny with a reason suits a call that is malformed, such as a missing key. An ask suits a call that is well formed but expensive, because it hands the decision to a person. An allow can skip the normal permission prompt, so use it only for read tools such as balance_get or jobs_list if you want them frictionless.

The hook may also return updatedInput to change the arguments before the tool runs. Be careful using that on a Sume call: silently adding or changing an idempotency_key or a spend cap changes what the user thinks was submitted. Prefer a visible deny and let the model resend.

What Sume does not do

The key is a transport and dedup value, not human approval, per Sume's docs. A hook that checks for its presence stops sloppy calls but does not ask a person. Use an ask decision if you want a prompt, and remember that Sume enforces max_spend_usd only when the caller provides it.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume