Claude Code PreToolUse hook example: gate paid Sume tools

A PreToolUse hook that lets dry_run previews through, denies paid Sume MCP calls with no max_spend_usd, and asks you before the rest. Script and settings.

5 min readSume
All posts

A PreToolUse hook is a script Claude Code runs before a tool call, and it can allow, deny or ask. For paid Sume tools, match the tool names with a mcp__sume__ regex, let dry_run previews pass, deny a submit that has no max_spend_usd, and ask you about the rest. Below is a settings entry and a short bash script that does exactly that.

Hook behavior is from Anthropic's hooks reference, read 2026-09-29. The tool names and the dry_run, max_spend_usd and idempotency_key arguments are from Sume's MCP tools and gates. The server is named sume because the MCP quickstart adds it with claude mcp add --transport http sume.

How do I write the hook?

Claude Code names MCP tools mcp__<server>__<tool>. A matcher that contains characters other than letters, digits, _, -, spaces, , and | is read as an unanchored JavaScript regular expression, so a parenthesized group works. Put this in .claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "mcp__sume__(generate_video|generate_image|tts_create|music_create)",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/sume-paid-gate.sh",
            "args": []
          }
        ]
      }
    ]
  }
}

What does the script check?

The hook gets the event as JSON on stdin, with tool_name and tool_input. Save this as .claude/hooks/sume-paid-gate.sh and run chmod +x on it. It needs jq. The two arguments it reads are top-level fields of a Sume tool call, as in the docs' dry_run and max_spend_usd example.

#!/bin/bash
IN=$(cat)
if [ "$(echo "$IN" | jq -r '.tool_input.dry_run // false')" = "true" ]; then
  exit 0   # preview only: normal permission flow
fi
if [ "$(echo "$IN" | jq -r '.tool_input.max_spend_usd // empty')" = "" ]; then
  jq -n '{hookSpecificOutput: {hookEventName: "PreToolUse",
    permissionDecision: "deny",
    permissionDecisionReason: "Paid call: send dry_run true first, then max_spend_usd."}}'
  exit 0
fi
jq -n '{hookSpecificOutput: {hookEventName: "PreToolUse",
  permissionDecision: "ask",
  permissionDecisionReason: "Paid Sume call with a spend cap. Approve?"}}'

What do allow, deny and ask do?

The deny reason goes to Claude, so word it as an instruction it can follow: here, preview first, then send a cap. Claude then repeats the call with dry_run true, which the script lets through.

From Anthropic's hooks reference, read 2026-09-29.
permissionDecisionEffectWho sees the reason
allowSkips the permission promptDebug log only
denyPrevents the tool callClaude
askPrompts the user to confirmThe user, not Claude
deferPauses so a claude -p caller can resume laterDebug log only

Why exit 2, and what if the hook fails?

Anthropic's page says exit code 2 is the code that blocks through the code alone for most events. Exit code 1 is a non-blocking error, and the action goes ahead. A hook that cannot start, such as a mistyped script path, lands in the same non-blocking bucket, so the gate is silently off. A hook that times out does not block either.

So run one paid-looking call after you install it and watch that the hook fires. Do not rely on a stalled hook as a gate.

  • The script above blocks with a JSON deny, not a bare non-zero exit.
  • Deny and ask rules in your permission settings still apply whatever the hook returns.
  • PreToolUse runs only when Claude calls a tool; it does not see files you pull in with @.

Why not a permission rule instead?

Rules cannot look at arguments for MCP tools. Anthropic's permissions page says Claude Code skips any mcp__ rule that has parentheses, so you cannot write "ask unless dry_run is true". A rule works per tool or per server; the hook is what reads the arguments. For the allow side, see Claude Code: allow MCP tools without approving every call.

None of this replaces Sume's own gates. idempotency_key is required on paid tools and max_spend_usd is enforced only when you send it, so the hook's job is making sure the model sends it. Wallet balance and admission still apply on every submit.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume