Follow Discord
Sweep 06 Nov 2026 · 01:32Z Build v2.1.319 537 read Stable v2.1.311 Latest v2.1.318 Next v2.1.319 Feeds RSS JSON llms.txt llms-full.txt Unofficial
Mods · 01 of 10

What a mod is

What a mod is, which builds load one, what changes on Bedrock and Vertex, and a mod you can run in four files.

Read against 2.1.286 Anthropic's page: Mods overview 22,923 characters 9 sections overview.json, paged

A mod is a plugin with a hooks module in it. Anthropic uses the word too, the folder in Claude Code's own repository is called mods/ and the cheat sheet they put out on 2026-09-09 is titled Claude Mods, the $ cheat sheet. Underneath it all is the function hook, one async function registered against one event, and Claude Code chains those functions together as middleware.

If you've written Express or Koa middleware you already know most of this. A hook gets three things, does what it likes with the first two, and decides whether to call the third:

export function register(on) {
  on("tool.call", { tool: "Bash" }, async ($, e, next) => {
    if (e.command.includes("rm -rf /")) return { deny: "no" }

    const r = await next(e)                    // every hook beneath, then core

    return { ...r, text: redact(r.text) }      // refine on the way up
  })
}

$ is the engine interface and it's the only way your code touches anything outside itself. e is the event, a flat object whose ids are fixed and whose payload you can rewrite. next is everything below you in the chain, handed to you as a function, and you can call it once, several times or not at all.

Everything on these pages was checked against Claude Code 2.1.286 on Linux. Where something was measured on another build, the page names that build.

Anthropic published its own mods documentation with 2.1.287, and every page here links the official page that covers the same ground, up top beside the build. Read theirs for the contract and the admin side, managed settings and the built-in guard, which this guide barely touches. Read this one for what the build does around it, the debug lines, the timings, the traps that each cost me an hour, and the methods their reference lists by name and says nothing else about. Where the two disagree, the page here says so and shows why.

On by default since 2.1.287

Up to 2.1.286, mods from plugins you install sat behind a rollout flag, tengu_plugin_hooks_modules, whose default in the shipped code was off, and you set CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 to load them. At 2.1.287 the default is on and the variable is ignored. I ran a one-hook mod in claude -p at 2.1.287 with CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=0 set, and it loaded anyway:

hooks module gatemod@inline loaded (worker, environment 2, tier user); events: session.start

So if you set it to 0 to keep mods off, that stopped working. Anthropic's overview has the ways that still do, disabling the plugin, --safe-mode, or disableAllHooks.

The flag didn't go away though. The 2.1.287 bundle still reads tengu_plugin_hooks_modules, it just falls back to on now, so Anthropic can still switch installed mods off from their side and you no longer have a variable to switch them back on with. Their own table of built-in mods admits as much, "Unless Anthropic has turned installed mods off remotely". When it's served off the debug log says so:

installed plugins' hooks modules not loaded: rollout flag (tengu_plugin_hooks_modules) is off, from GrowthBook (this session's payload); built-in plugins load regardless

That line is from the 2.1.286 run that found the gate, and the 2.1.287 bundle builds the same text. The bit after is off, names where the value came from. With the flag off the plugin itself still loads, its commands and skills work, and only the hooks module is dropped. This page used to say that since 2.1.280 the same reason also reached you as a notification. The 2.1.287 bundle writes the rollout line to the debug log and nowhere else. The other lines that say why a mod didn't load or a hook was skipped go to the debug log in an ordinary terminal session, to the transcript in a session that hot-reloads a mod, and to stderr under claude -p for a mod loaded with --plugin-dir. A probe mod of mine that threw in claude -p --plugin-dir printed modb: session.start hook skipped: threw TypeError: No default value straight to the terminal. Anthropic's troubleshooting page has the same split in Find out why a mod does nothing.

Anthropic's own mods ignore the flag. Since 2.1.275 it only covers installed plugins, and the built-in ones load either way. Before that, a shut gate stopped every mod including Anthropic's.

A few other things turn installed mods off: disableAllHooks in managed settings, disableAllHooks in your own ~/.claude/settings.json (which leaves whatever your organization manages running), allowManagedHooksOnly, --bare, --safe-mode, and a workspace you haven't trusted yet. Each has its own debug line and they're all on the gotchas page.

To check whether mods can load on a machine at all, run claude plugin test in an empty folder, which is Anthropic's own check. At 2.1.287 an empty folder on my machine exits 1 with no hooks module to load; there is no hooks/hooks.json naming one in "modules", which is the answer that means mods can load. Their page lists the reasons it prints when something did.

Bedrock, Vertex and other providers

Mods work the same on Bedrock and Vertex, with three differences worth knowing before you ship one to people on those providers.

The remote switch can't reach you. The rollout flag comes from GrowthBook, and Claude Code switches GrowthBook off completely when it isn't talking to Anthropic's own API. That covers Bedrock, Vertex, an LLM gateway, and anyone who sets DISABLE_TELEMETRY, DO_NOT_TRACK or CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC. With GrowthBook off the flag sits at its default. Up to 2.1.286 that default was off, so an installed mod on Bedrock only ever loaded with CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1. At 2.1.287 the default is on, so mods load there with nothing set, and the one switch that could turn them off from Anthropic's side never gets a value. The one exception is CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST, which keeps GrowthBook running.

$.session.usage() has no rate limits. rateLimits is filled from the subscription headers on Anthropic's responses, and Claude Code only reads those for a Claude.ai subscriber or a gateway. On Bedrock, Vertex or a plain API key it's always an empty array, so a quota guard has nothing to guard against there. context and cost are still filled in.

$.model.complete and $.model.fork go through the session's own client, so on Bedrock they're billed to your AWS account and a model alias resolves through the Bedrock model mapping like any other request. The hooks code has no provider check of its own. Whether a given model is allowed is up to that client, and a refused model comes back as a rejected call.

The first is read out of the 2.1.287 bundle and the other two out of 2.1.283. I haven't run a mod on Bedrock or Vertex myself, so treat this section as unverified until someone does.

It changes without notice

The declarations Claude Code writes about itself open with a line you should take literally:

EARLY ACCESS: this surface may change between releases without notice.

It does. Between 2.1.263 and 2.1.272 the filesystem calls were renamed from fs.readFile, fs.writeFile and fs.listDir to fs.read, fs.write and fs.list with no deprecation, and every mod using the old names broke. Between 2.1.272 and 2.1.283 $.model.complete stopped returning a string and started returning a result object, so anything that treated the reply as text broke too. Between 2.1.283 and 2.1.286 every built-in mod was renamed with a cc-plugin- prefix, and $.telemetry moved out of the telemetry mod and into the engine itself. The declarations name the build that wrote them on their first line, // Written by Claude Code 2.1.286., so pin the build you tested on and read them again when you upgrade. The early-access line is still there at 2.1.287, the release the official docs went up with, and Anthropic's own create page tells you to trust those files over any page when they disagree, theirs included.

Claude Code's own features are mods

The best evidence this runtime is real is that Anthropic builds on it. The 2.1.286 binary registers eleven built-in mods: cc-plugin-sec-default, cc-plugin-agents-md, cc-plugin-telemetry, cc-plugin-plugin-authoring, cc-plugin-mods-guide, cc-plugin-tips, cc-plugin-mermaid, cc-plugin-responsive-mode, cc-plugin-diff, cc-plugin-you-should-know and cc-plugin-claude-test. I'll drop the prefix from here on.

Only four of them loaded a hooks module in my sessions at 2.1.286: sec-default, agents-md, telemetry and diff, in claude -p and in the terminal alike. The rest didn't load one for me. The two I read in the bundle, mermaid and mods-guide, sit behind rollout flags that default to off, and I haven't checked what holds back the others. At 2.1.287 a claude -p run built $ for three of them, sec-default, agents-md and telemetry, and Anthropic's table has diff loading in interactive terminal sessions only. Their overview lists the built-ins with how to turn each off, and the source of four of them is public in the mods folder of the Claude Code repository.

Mod What it is
diff The /diff pane beside the transcript, redrawn as Claude edits
sec-default The org security baseline, seated outermost on managed machines
telemetry Queues Anthropic's first-party telemetry rows. Since 2.1.284 $.telemetry is part of the engine and no longer comes from this mod
plugin-authoring The "Make a mod" skill, new in 2.1.283, listed as /plugin-authoring at 2.1.286
mods-guide A small panel with Clawd cooking, drawn over the question card when you ask how hot reloading works. From the 2.1.286 bundle, I haven't seen it run
you-should-know New in 2.1.286 and off by default. While Claude works it offers "one small thing worth learning from the conversation, above the prompt", and it isn't available to HIPAA, ZDR or LDR accounts. From the 2.1.286 bundle, I haven't turned it on

Anthropic's overview describes telemetry as a mod that "adds methods that other mods can call, and ships their types". At 2.1.287 the declarations put telemetry on the engine interface itself, beside config and the rest, and all three of my 2.1.287 runs logged engine.create: no plugin-provided interfaces, so no plugin was adding methods to $ in those sessions. The built-in mod still hooks telemetry.log and queues the first-party rows.

At 2.1.272 the diff pane's register.ts alone was 900-odd lines using eleven different events. Every one of those is available to your mod too, there's no private API propping it up.

sec-default is worth a note, because it doesn't run on a normal personal install at all. It loads on a machine with managed settings, or for a user signed in with a Team or Enterprise plan. Managed prependPlugins works as a filter on top of that. Once the list is set, the guard loads only if the list names it, at the position listed. That's from Anthropic's reference, and it replaces what I had here before, which counted prependPlugins as a third way in. Its options, and how an admin keeps or drops it, are on their admin page. This machine has managed settings and no prependPlugins, and a 2.1.287 debug log here says why the guard loaded:

cc-plugin-sec-default@builtin seated outermost: this machine has managed settings
hooks module cc-plugin-sec-default@builtin loaded (native, environment 1, tier prepend); events: classic.*,prompt.section,...

The fold

Hooks nest. The engine composes them into one function, and the composition is an onion with core in the middle:

X = A ∘ B ∘ C ∘ core = A(B(C(core(⊥))))

Whatever registered earlier wraps more, so position is authority. On the way down each hook can change the question, core answers it, and on the way back up each hook can change the answer. There are five tiers and the tiers page covers them properly, but for now just hold on to this: your hook sits somewhere in a stack, and whatever sits beneath you is a function call.

The cheat sheet gives two rules as a pair, and between them they're the whole contract for next:

core has a side effect: no next = it did not happen, next twice = it happened twice

core has no side effect

tool.call falls under the first one. Calling next is what runs the tool. Returning without calling it is what stops the tool, and returning { deny: "..." } after you already called it tells the model the call failed while the file is sitting on disk. Anthropic said as much in the issue thread:

The return of 'deny' here is not somehow making the tool not get called. To make the tool not get called, don't call next(e). When you hook onto tool.call, you are responsible for calling the tool.

Where the code runs

Installed mods share one worker thread, and each plugin gets its own numbered environment inside it. I read the debug line the other way round for a while. It says "one for every plugin", and it means one worker that every plugin shares. A 2.1.287 run with two probe mods, moda and modb, logged the spawn once and put the two mods in environments 2 and 3, with Anthropic's own mods running natively around them:

hooks module cc-plugin-sec-default@builtin loaded (native, environment 1, tier prepend); events: classic.*,...
hooks worker spawned (one for every plugin)
hooks module moda@inline loaded (worker, environment 2, tier user); events: session.start
hooks module modb@inline loaded (worker, environment 3, tier user); events: session.start
hooks module cc-plugin-agents-md@builtin loaded (native, environment 4, tier builtin); events: session.start,prompt.context,agent.spawn,tool.call
engine.create: no plugin-provided interfaces; $ built for cc-plugin-sec-default,moda,modb,cc-plugin-agents-md,cc-plugin-telemetry

Sharing a thread means a hook stuck in a loop that never awaits holds up every other installed mod. When Claude Code can trace a worker crash to one mod it unloads just that one. When the worker has crashed three times with no mod to blame, it unloads every mod that isn't built in for the rest of the session. Anthropic's troubleshooting page has both messages under it crashed the hooks worker.

Under the hood it's a Bun worker with a node:vm wrapper per plugin, though Anthropic is clear that's not a promise. What they do promise is that you get nothing ambient. No process, no require, no Bun, no global fetch, no Worker, no navigator, no DOM. Import node:fs or node:child_process and the loader refuses with "a hooks module imports its own files by relative path and "claude-code", nothing else". WebAssembly is deleted from the environment on purpose, which is what's actually tripped people up trying to build games above the prompt.

What's left is plain ES2023 on web APIs, crypto, structuredClone, typed arrays, URL, TextEncoder, console and the rest of the standard library. I counted 87 globals at 2.1.272 and 81 at 2.1.287. eval is in the list but throws EvalError: Code generation from strings disallowed for this context when you call it, and queueMicrotask isn't there at all.

Timers are where the two sets of docs and the build part ways. Anthropic's API page says a hooks module has "no timer globals such as setTimeout", and the declarations say the same. At 2.1.287 my probe mod found setTimeout, setInterval, clearTimeout and clearInterval all defined, and a setTimeout of 200 ms raced against $.clock.sleep(2000) won:

[modb] $.ui.log (to debug): PROBE-B setTimeout fired after 202ms; eval threw EvalError: Code generation from strings disallowed for this context

Use $.clock.after and $.clock.every anyway. They're the documented route, a hot reload cancels them with the old module, and a timer global that the docs say isn't there can go in any release. I haven't checked what a bare setTimeout does across a reload.

Anything that reaches outside the process goes through $. That's deliberate, since $ is the plugin's whole list of capabilities and an organisation can take nouns off it.

Every file in a mod has to be named .ts, .tsx, .jsx, .js, .mjs, .cjs, .mts or .cts, and all of them are treated as ES modules. A file with any other name isn't loaded.

These aren't the settings hooks

Claude Code already had hooks, the ones you set up in settings.json that run a shell command and trade JSON over stdin and stdout. Those still exist and still work. Function hooks are a separate mechanism that sits on top of them, every settings hook is wrapped one for one as classic.<Event> with its exact JSON in and out, and on a Bash call the tool.call chain starts first and finishes last, so it wraps the PreToolUse hooks from your own settings files and from plugins' hooks/hooks.json completely. Managed settings are the exception. Anthropic's events page puts a managed PreToolUse hook before the first mod's tool.call hook and makes its block final, so no mod sees that call. I haven't measured that one, because the managed settings on this machine carry no hooks.

In practice the difference is speed and what you can do. A settings hook launches a process. At 2.1.260, twenty-five pass-through function hooks added 5 ms over having none, while one command hook sleeping 50 ms turned a 15 ms dispatch into 166 ms. Function hooks run one after another though, which cuts the other way. Eight of them each sleeping 300 ms took 2,427 ms, where eight command hooks doing the same took 640 ms because they run in parallel. Running in order is what makes the onion work, so that's the price.

The bigger difference is that a function hook can read and rewrite the value passing through it, in both directions, with types. The old hooks can't, and that's most of why this runtime exists.

The smallest mod that does something

Four files, and the README is optional:

my-mod/
  .claude-plugin/plugin.json     { "name": "my-mod", "version": "0.0.1", "description": "..." }
  hooks/hooks.json               { "modules": ["./hooks.ts"] }
  hooks/hooks.ts                 export function register(on) { ... }
  README.md
import type { On } from "claude-code"

export function register(on: On) {
  on("session.start", async ($, e, next) => {
    $.ui.log(`mod up in ${e.cwd}`, { to: "debug" })

    return next(e)
  })
}

Run it with claude --plugin-dir ./my-mod. That folder reloads when you save, and since 2.1.277 only the modules you actually changed are reloaded. When Claude Code loads a mod from a folder you own it also writes the type declarations beside it, in .claude-plugin/types/, along with a tsconfig.json, so your editor has the types from the first run. It does that under claude -p too. You need TypeScript 5.4 or newer to read them. At 2.1.287 that folder also holds the built-in tool shapes, and the /plugin-types command that used to write them is gone. Anthropic covers it in Get the types for your build, and the anatomy page lists what lands in the folder.

2.1.283 adds a second route. The built-in "Make a mod" skill writes a mod into a per-session folder, asks "Enable hot reloading for this session?", and from then on reloads it whenever its files change. The folder comes back when you resume the session. Anthropic's create page names it as ~/.claude/dev-mods/ followed by the session's ID, with one directory per mod inside.

This site's 2.1.284 changelog has three changes to that question, and the 2.1.286 bundle agrees with all three, though I haven't seen any of them on screen. "How does this work?" is now the first choice, ahead of "Enable for this session" and "Not now", and it explains up to three times before a fourth ask counts as Not now. If you decline, Claude is told what it wrote isn't loaded and loads the next time the session starts. It's also told that /reload-plugins reloads only a mod that loaded at the start, so that command won't pick up a mod Claude has just written. That only covers a mod Claude writes mid-session. Anthropic's overview has /reload-plugins loading a mod you install or update from your shell while a session is open, and I haven't tested either case. Anthropic's create page lists two answers to the question, "Enable for this session" and "Not now". The 2.1.287 bundle still builds three, with the explainer first:

[{value:"explain",label:"How does this work?"},{value:"allow",label:"Enable for this session"},{value:"not_now",label:"Not now"}]

A copy installed somewhere else doesn't reload. One under ~/.claude/skills/ kept serving stale code through three runs at 2.1.270 and only picked up the edit after a restart, so check that before you spend an hour debugging a change that did apply.

What's here and what isn't

The next two pages are the model, how a mod is put together and how the chain composes. After that it's reference and you can read the pages in any order. Read the gotchas page before you ship anything, because this runtime doesn't fail the way its shape suggests and one of those failures turns a security guard into a no-op.

What isn't here is every field of every event. The declarations run to 14,916 lines at 2.1.287, and the types folder adds another 5,088 lines of built-in tool shapes you already know from using Claude Code. Where a page states a behaviour it either quotes the declarations of a named build or names the run behind it, and where I couldn't measure something I say so instead of repeating what someone else measured on an older build.

Feedback