Skip to main content

How Claude Code Mods Hook Into the Tool's Events

Updated 6 min readMarkdown

Anthropic shipped Mods on 1 October 2026, in Claude Code 2.1.287 (2.1.286 in the Desktop app). On by default. A mod is a TypeScript or JavaScript function that runs inside Claude Code and hooks into the events the tool already fires — a tool call, a submitted prompt, a piece of the interface being drawn. The docs' own one-liner: "a plugin that changes how Claude Code looks and behaves."

It isn't the same thing as the settings hooks Claude Code already had, and the difference is where the code runs.

Settings hooks run outside. Mods run inside

A settings hook is a shell command, an HTTP request, or a prompt that Claude Code fires at a lifecycle point. It runs outside the tool, in its own process. A mod is a plugin whose code Claude Code loads into its own process. Because it lives inside, it can do things a settings hook can't: draw a pane beside the transcript or a band above the prompt, redraw Claude Code's own interface (a tool call's row, the spinner, the question dialog), hold a tool call while it asks you something, and run a command with no model turn at all.

The plumbing is small. A minimal mod is three files: plugin.json (the manifest), hooks.json (points at your code file), and register.js or register.ts (the hooks module). No Node install, no bundler, no build step; Claude Code loads the file directly.

The module exports one function, register(on). Every handler gets three arguments: $ (the mods API), e (the event as plain data), and next (passes the event to the next mod, then to Claude Code). A hook does one of three things: observe and call next(e) unchanged, rewrite by calling next with a changed copy, or answer — return its own result, never call next, so the usual behaviour never runs.

What a hook can actually touch

The event names are the interesting part, because they map to moments in a session you already know. tool.call fires each time Claude is about to use a tool. ui.render fires each time Claude Code draws the spinner. session.start fires when a session starts. command.run fires for a slash command — with a matcher like { command: 'tally' } so the hook only runs for that one command. Anthropic's own built-in mods hook more: the AGENTS.md loader hooks prompt.context to hand instruction files to the model, and agent.spawn when a subagent forks.

Mods share state through the variables in their file. Here is a small mod in that shape — it counts Bash calls, shows the count beside the spinner, and adds a /bash-tally command, written from the docs' description of the first-mod tutorial:

import type { Hook, ModsAPI, Next, ToolCallEvent, UiRenderEvent } from "claude-code";

type On = import("claude-code").On;

let bashCalls = 0;

const countBash: Hook<ToolCallEvent> = async (_$, e, next) => {
  if (e.tool === "Bash") bashCalls += 1;
  return next(e); // observe: let the call run as usual
};

const showCount: Hook<UiRenderEvent> = async (_$, e, next) => {
  return next({ ...e, suffix: `bash calls: ${bashCalls}` }); // rewrite
};

const printTally: Hook<{ command: string }> = async () => {
  return `Bash calls so far in this session: ${bashCalls}`; // answer
};

export function register(on: On): void {
  on("tool.call", countBash);
  on("ui.render", showCount);
  on("command.run", printTally, { command: "bash-tally" });
}

Two honest caveats. First, I typechecked this on my machine with TypeScript 7.0.2 and it compiled clean (exit 0) — but against my own stand-in type declarations, written from the docs. The real types are generated into .claude-plugin/types/ when Claude Code loads a mod, and the real check is claude plugin validate. Second, I have not run this inside Claude Code; the shape above follows the docs' prose, not a session I watched. If your generated claude-code/index.d.ts disagrees with my stub, trust the generated file.

Load it for one session with claude --plugin-dir ./my-mod; Claude Code hot-reloads the hooks module when a file changes.

The part that should make you pause

Mods are not sandboxed. The docs say it plainly. A loaded mod can read and write files anywhere your user account can, start programs, make network requests, read environment variables and settings files including API keys, see every prompt you send, rewrite a prompt or a tool call, approve a tool call before you're asked, and spend your usage by calling a model on your plan.

One limit worth knowing: a mod can restyle much of the interface but it cannot change the permission prompt. Another: turning on sandboxing doesn't fence mods in — the sandbox isolates the Bash commands Claude runs, and a process a mod starts runs outside it.

Anthropic's answer is a built-in guard mod, cc-plugin-sec-default. On Team and Enterprise plans it guards org-managed settings from user-installed mods — with it loaded, a user mod can't approve tool calls your deny or ask rules would stop. The admin sets the guard's order in managed settings, and a user can't turn it off. That ordering is the whole point: org policy hooks run before user-installed ones get a say.

Some of Claude Code's own features are already mods — /diff is one, which is why you can turn it off or replace it. Anthropic also publishes three sample mods in its claude-code-playground repo: token-weather, blast-radius, and replay-theater. They're the fastest way to see the event model in real code, and to see what claude plugin validate prints for a mod you didn't write.

My rule after reading the docs end to end: treat a mod like a browser extension from an unknown author. The capability is real, but the code runs as you, sees everything you type, and nothing walls it off. Read the hooks: and calls: lines first. Then decide.

Sources: Anthropic's Mods overview and "Create a mod" pages at code.claude.com/docs/en/plugins/mods (read 10 Nov 2026). The bash-tally example was typechecked with TypeScript 7.0.2 against my own stub declarations, not Anthropic's generated types.


P.S. Everything I write stays free — that part doesn't change. But if this one was worth something to you, you can buy me a coffee ☕ (one-time or monthly, both work). It covers the API credits and hosting behind these experiments — and honestly, it's also the nudge that gets the next one started on the evenings I'd otherwise skip it. — Salam

Buy me a coffee

2 views

No comments yet. Be the first to comment.