React to events with a mod changedplugins/mods/events
Nearest release: v2.1.295, published under an hour after upstream edited the page. Shown because the two are within 24 hours of each other. Nothing here says the release caused the edit.
Upstream edited this page at 8 Oct 2026 18:22 UTC, give or take a minute or two: the time comes from Anthropic’s own sitemap rather than from a commit. This site recorded the change at 8 Oct 2026 18:37 UTC.
Upstream edited
Recorded here
Lines+16added
Lines−2removed
From line
307
where the diff opens
First seen
1 Oct 2026
this site's first read of the page
Recorded edits7to this page, all time
The whole hunk
from line 307, old and new numbered
/
from line 307
307307
308308One line names 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 session, as [Find out why a mod does nothing](/docs/en/plugins/mods/troubleshoot#find-out-why-a-mod-does-nothing) lists. A `ui.render` hook whose drawing doesn't validate is reported differently, as [Build a tree from elements](/docs/en/plugins/mods/interface#build-a-tree-from-elements) describes.
309309
310To make a hook that blocks calls fail closed, add a `.catch` error handler that answers in its place. Here, `guard` is your hook function:
310To make a hook that blocks calls fail closed, add a `.catch` error handler that answers in its place. Here, `guard` is your hook function, and the handler tests [`next.called`](/docs/en/plugins/mods/reference#the-hook-function) to tell whether `guard` had already called `next` when it failed:
311311
312312```javascript theme={null}
313313// on returns a registration, and .catch attaches a handler to that one hook
314314on('tool.call', { tool: 'Bash' }, guard).catch(async ($, e, next) => {
315 // guard had already called next, so return what came back
316 if (next.called) return next(e)
315317 // next.error.kind is 'throw' or 'timeout', which says how guard failed
316318 return { deny: 'The command guard failed, so this command was not run: ' + next.error.kind }
317319})
318320```
319321
320While `guard` works, the handler never runs. When `guard` throws or times out on a Bash call, Claude Code calls the handler with the same event. The handler returns `{ deny }`, so the command doesn't run, and Claude reads the text with `throw` or `timeout` at the end. Without the handler, Claude Code would skip `guard` and run the command. The handler has a shorter [time limit](/docs/en/plugins/mods/reference#limits) of its own.
322While `guard` works, the handler never runs. When `guard` throws or times out on a Bash call, Claude Code calls the handler with the same event:
323
324* **`guard` failed before it called `next`**: the command doesn't run, and Claude reads the `deny` text with `throw` or `timeout` at the end
325* **`guard` failed after it called `next`**: the handler's `next(e)` resolves to the result that `guard`'s call produced without running the command again, and Claude reads that result
326
327The handler has a shorter [time limit](/docs/en/plugins/mods/reference#limits) of its own. If the handler itself throws or times out, Claude Code skips the hook as if it had no handler. When `guard` hadn't called `next`, the command then goes on as it would without the mod.
328
329The same handler shape fits a guard on `prompt.submit` or `config.set`. When `next.called` is false, return the refusal that the [events reference](/docs/en/plugins/mods/reference#events) lists for that event: `{ drop: 'the reason' }` for `prompt.submit`, `{ deny: 'the reason' }` for `config.set`.
330
331At `tool.check` and `plugin.register`, a refusal returned after `next` resolved still holds, so return it without testing `next.called`:
332
333* **`tool.check`**: return `{ decision: 'deny', reason: 'the reason' }`
334* **`plugin.register`**: return `{ refuse: 'the reason' }`, as [Refuse mods when your check fails](/docs/en/plugins/mods/admin#refuse-mods-when-your-check-fails) shows
321335
322336## Next steps
323337
No line in this hunk matches that.