Claude Code mod hook skipped and a paid Sume call still ran
A Claude Code mod hook that throws or times out is skipped, so the call runs anyway. Add .catch to fail closed, and know what a mod still cannot guarantee.

A Claude Code mod hook that fails before it calls next is skipped, and the next handler runs in its place, so a guard that throws or times out lets the paid Sume call through. That is the documented default, not a bug in your code: Claude Code's events guide says a hook that fails "doesn't break the session" and that without a .catch handler the call would run. The fix is to attach .catch so a failed guard answers with a deny.
Details here come from Claude Code's events guide and organization guide, read 2026-10-03. Sume's paid tool names come from tools and gates.
How does Claude Code treat a failing hook?
Claude Code's docs give a short list of failure cases. The table collects them, including two that sit outside any single hook and that a .catch handler cannot rescue.
| What happened | Result |
|---|---|
| The hook threw, timed out, or returned the wrong shape before calling next | Claude Code skips it and the next handler runs; the tool call proceeds |
| The hook failed after next resolved | That result stands and nothing runs a second time |
| The hook has a .catch handler and failed before next | The handler is called with the same event and can answer, for example with a deny |
| The thread running installed mods crashed three times | Claude Code unloads every mod that is not built in until /reload-plugins or a new session |
| A user starts Claude Code with --safe-mode | Installed mods do not run |
What does a fail-closed guard look like?
This guard denies a paid generate call that carries no max_spend_usd, a field Sume enforces only when it is sent. The .catch handler runs only when guard throws or times out, and next.error.kind is throw or timeout. The handler has a shorter time limit of its own.
async function guard($, e, next) {
if (!e.max_spend_usd) return { deny: 'Add max_spend_usd to this paid Sume call.' }
return next(e)
}
export function register(on) {
on('tool.call', { tool: /^mcp__sume__generate_/ }, guard)
.catch(async ($, e, next) => {
return { deny: 'Sume guard failed: ' + next.error.kind }
})
}Where do I see that a hook was skipped?
Claude Code logs one line naming the mod, the event, and the reason, such as my-mod: tool.call hook skipped: threw Error: boom. Where you read it depends on the kind of session; the events guide points to its troubleshooting page for the list. If you see that line next to a Sume call that should have been refused, the guard failed open. The hooks-not-firing post covers the other common cause, a matcher that does not match the MCP tool name.
If you are testing the guard, force a failure on purpose: make guard throw on a call that you know will arrive, then confirm in a throwaway session that the deny text comes back to Claude and that no job was created. Check jobs_list afterward rather than trusting the transcript alone. A guard that has never been seen failing closed has not been tested.
What is the time limit trap?
A hook that waits has a time limit, and a timed-out hook is skipped. Claude Code says time inside a mods API call such as $.ui.ask does not count against the limit, but time you spend awaiting a promise of your own does. So a guard that fetches a price quote over the network with its own await can time out on a slow day and, without .catch, let the call through. Keep guards synchronous where you can, as the one above is.
What can a mod never guarantee?
Even a correct fail-closed guard has limits. These are the ones to plan around, each tied to something Claude Code or Sume documents.
- That it is loaded. A crash loop,
--safe-mode,disableAllHooks, or a managed policy can all leave a session without it, and.catchcannot help a mod that is not running. - That it covers every client. A script or another editor using the same Sume key never meets it.
- That it knows the price. Sume's spend gate is wallet and admission;
dry_run=truepreviews it andmax_spend_usdcaps a call only when sent. - That it covers
script_run's child calls, which run on the Sume side.
What should be the real limit?
Put the hard limit where the money is. With OAuth and Write off, Sume's paid tools are hidden and return insufficient_scope (OAuth and API keys); the wallet balance bounds what any session can reserve; and for unattended jobs, Agent Completions require a generation_spend_cap_usd on each call. Use the mod as a seat belt on top of those.
Sources
Related posts
More in Developers
- Claude Code mod: ask before a paid Sume call (and claude -p)
A Claude Code mod can hold a paid Sume MCP call with $.ui.ask and show max_spend_usd in the question. In claude -p nobody answers, so it refuses. Code inside.
- Claude Code token usage vs Sume spend: two meters, one session
Claude Code's token totals and Sume's generation spend are billed in different places. Log tokens with a mod, read Sume's wallet with usage_get, and compare.
- claude plugin validate: read the hooks and calls lines of a mod
Before installing a Claude Code mod, run claude plugin validate and read its hooks and calls lines. Which combinations matter when Sume's MCP is connected.
- Contract-test Sume API responses against openapi.json (pytest)
Validate recorded Sume responses against the OpenAPI schema with jsonschema, including the OpenAPI 3.0 nullable fix. A tested pytest file and fixtures guide.
Written by Sume