Claude Code’s extension model is changing shape: from a set of independent hooks that vote on an operation, to an ordered stack of mods that wrap it. That one change is why Claude Code is about to have an admin story it has never had — ordering creates precedence, and precedence is what authority is made of. The machinery is already in the binary on your machine, behind a flag, with an API that may change without notice.
Primary sources, read in full: issue #91870 (body + 168 comments) · the three built-in mods’ source · hooks docs · plugins docs · the v2.1.270 changelog and binary
Claude Mods are not shipped and not stable. The machinery is in the CLI you
already have, behind CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1, and the README that ships with
it says the API “may change between releases without notice” — against a release
cadence of roughly one a day. So this course marks exactly one distinction, and it is the only one
you need: stable means the plugins and hooks surface that is documented today and
that you can build a team on; unstable means the mods surface, which you should
experiment with and should not depend on.
Everything else here is about the architecture, because the architecture is the part that will survive the renaming. Mods replace a set of independent, parallel hook handlers with an ordered stack of wrappers — and almost everything interesting about them, including the first credible admin story Claude Code has had, falls out of that single change.
Short on time? Module 3 for the design decision that explains everything else, and Module 8 to find out whether it stuck.
“A mod is a Claude Code plugin whose behaviour lives in a hooks module: oneregister(on, options)entry that hooks the engine’s events as functions($, e, next).” anthropics/claude-code,mods/README.md
Three things are packed into that. A mod is a plugin — it installs the way
plugins already install. Its behaviour lives in one module with one entry point, rather
than in a scattering of shell commands wired to event names. And its handlers take
next, which is the entire story: a handler receives the rest of the
system as a callable and decides what to do with it.
Today’s hooks are voters. Each one is handed an event, runs independently of the others, and returns a verdict: allow, deny, or say something. They do not see each other, they have no defined order, and none of them is around the operation — they are beside it.
A mod is a wrapper. It is handed the event and a continuation. Run code, call
next, and the rest of the system proceeds inside your call. Which means you get the thing
a voter can never have: the return journey. You are still on the stack when the result
comes back, so you can read it, change it, or decline to have made the call at all.
A set of voters has no inside and no outside. Two plugins that both want to filter the same tool call are peers: whichever the runtime happens to invoke first wins, and neither can rely on that. There is no way to express “my rule applies to yours”, so there is no way to express organisational authority — which is why Claude Code has had plugins for a long time without having an admin story worth the name.
An ordered stack of wrappers has an inside and an outside, and that is a structural fact rather than a policy: whatever registers first wraps everything after it. Once that is true, four things become possible that no amount of policy language could deliver on top of voters:
| What ordering buys | Why a voter cannot do it |
|---|---|
| Transform a result on the way back | A voter’s decision is made before the operation runs. It never sees what came back in the same breath as the decision it made about it |
| Guarantee you run before another plugin | Voters are unordered peers. “Run first” is not expressible |
| Observe everything, including other plugins’ effects | A voter only sees the events it subscribed to. It is never outside another plugin, so another plugin’s calls are invisible to it |
| Remove an affordance so nothing beneath can use it | A voter can refuse to do a thing. It cannot take the ability to do that thing away from its peers |
That last one is the sharpest, and it is worth holding on to now because it recurs through the rest of the course. Because every side effect a mod may perform arrives through an object it is handed, an outer mod can hand the inner ones a version of that object with a capability missing. The inner mod is not blocked from doing the thing; it has no way to express the thing. That is capability removal by construction, not by policy checking — and it is the difference between a rule and a wall.
Stable: the plugins and hooks surface documented today. Build on it, standardise on it, put it in front of a team.
Unstable: mods. The machinery is in the v2.1.270 CLI — the flag
CLAUDE_CODE_ENABLE_FUNCTION_HOOKS, along with plugin-types,
engine.create, next.to and sec-default, are literal strings in
the shipped binary — and three mods written against it ship inside the CLI. It is absent from the
documentation and named nowhere in the v2.1.267–270 changelogs. The README is explicit: hooks
modules “load only where function hooks are enabled, and the API these mods are written against
may change between releases without notice.”
That sentence is the whole risk calculus. “Without notice” is not boilerplate when four releases went out in the four days around the feature’s renaming. Run it, read it, feed back on it — and treat anything you build on it as something you have volunteered to maintain against a moving target.
Two opposite misreadings circulate and both cost you something. “Mods have shipped” will surprise you with a breaking change on a Tuesday. “It is just a discussion” is the more expensive error: the code is in the binary you already have, the design is being argued out in public right now, and treating it as vapour means learning the shape of it six months after you could have influenced it.
register, handlers as ($, e, next)A plugin is a directory with an optional
.claude-plugin/plugin.json manifest that can bundle any of:
| Directory | What it holds |
|---|---|
skills/ | Model-invoked capabilities as SKILL.md folders; namespaced /plugin:skill |
agents/ | Custom subagent definitions |
hooks/hooks.json | Event handlers |
.mcp.json | MCP server configuration |
.lsp.json | Language servers for code intelligence |
monitors/ | Background watchers that notify Claude as events arrive |
bin/, settings.json | Executables on the Bash PATH; default settings when enabled |
Hooks today are configured as JSON with a matcher, and each
handler is one of five types: command (a shell command receiving JSON on
stdin), http, mcp_tool, prompt, and agent
(experimental). They fire on a long list of lifecycle events — PreToolUse,
PostToolUse, UserPromptSubmit, SessionStart,
PermissionRequest, SubagentStop and many more — and they decide by exit
code or by emitting JSON.
Hold property 1 especially. Almost everything mods change follows from replacing it.
($, e, next) signature and what each part is for$ object is and why side-effect tracking is the safety storyThe author’s own summary: “Function Hooks let you
modify CC very deeply, while still being safe through side-effect tracking over a parameterized
$ object, and while composing neatly using a registration-order-based
‘next’ continuation model a la Express, or Koa.”
From the mods README, the shipped description: “A mod is
a Claude Code plugin whose behaviour lives in a hooks module: one register(on, options)
entry that hooks the engine’s events as functions ($, e, next).”
$.fs, $.session, $.ui, $.settings, $.env, $.http, $.clock and more. Because effects go through $ rather than around it, they can be observed, and they can be removed.From the advanced series: “Plugins nest like middleware. The first one registered wraps the rest, so admins prepend for control and append for defaults.”
TypeScript and LSP support are the headline, and they are the least interesting part. The change that matters is that extension moves from a set of independent voters to an ordered stack of wrappers. Ordering creates precedence; precedence creates the possibility of authority; authority is what makes an admin story possible at all. Module 4 is that consequence.
| Capability | Today’s hooks | Function hooks |
|---|---|---|
| Deny an operation | Yes | Yes |
| See the result and transform it before the caller reads it | Only via a separate Post event, with no link to the Pre decision | Yes — you are around the call |
| Replace the operation with your own | No | Yes — return a value instead of calling next |
| Guarantee you run before another plugin | No — parallel | Yes — registration order is nesting order |
| Observe every event including other plugins’ calls | No | Yes — a hook on * |
| Change what is rendered | No | Yes — hook components, modify props or wrap render nodes |
On the wildcard: “A single hook on * sees
every event, including every plugin’s own calls on $, so an audit log is one
function.” For anyone operating under an audit regime that
is the most consequential line in the entire proposal, and Module 5 shows it already has a
shipped consumer.
($, e, next): the effect interface, the typed event, the continuation$, so an outer hook can remove an affordance from everything beneathThis is not speculation about a future admin story. The
sec-default mod ships inside the CLI and its README states the model plainly.
sec-default is seated here on a machine with managed settings, or for a Team or Enterprise organisation, “unless managed prependPlugins says otherwise.”sec-default actually doesIts README is unusually precise about scope: it “keeps an organization’s classic hooks, prompt content, managed settings and tool policy out of reach of the plugins a person installs; adds no policy of its own. Everything else passes through untouched.”
Three moves and nothing else: continue past the user tier
(next.to(e, "append")), refuse a user-tier caller by name
({ deny } when next.origin.tier is user), or pass
(next(e)). What it protects: classic.* events,
prompt.section / prompt.context / skill.prompt /
attribution.text, settings.read, and
tool.describe / command.describe / agent.offer /
agent.spawn.
“A subject’s provenance is the event’s
pinned e.provider; policy is read through
$.settings.read({ source: "policy" }), one read serving a burst; both fail closed,
so an unreadable policy counts as a policy in force.”
Fail-closed on unreadable policy is the correct default and it is worth noticing that it was chosen. A system that fails open when it cannot read its own rules is one that can be disabled by breaking it.
The README says it directly: “Some of what an organization sets today (its classic hooks, its managed CLAUDE.md and rules, its settings, its MCP allowlist) was never within a person’s reach before function hooks.”
Read that carefully, because it inverts the usual reading.
sec-default is not new power for administrators. It is a patch for power that
function hooks would otherwise hand to users — the ability to reach org settings that were
previously out of reach simply because no extension mechanism could touch them. The org tier restores
the status quo; it does not extend it. Anyone selling mods to an organisation as “new control”
has the direction backwards.
sec-default ships in the binary, seated outermost for managed/Team/Enterprise machinesThree mods ship inside Claude Code, with source published at
mods/: sec-default, diff and telemetry. The README
calls this folder “their source, published as it is built into the binary.”
| Mod | What it does | What its existence tells you |
|---|---|---|
sec-default |
Keeps org settings out of reach of user plugins. Seated outermost on managed / Team / Enterprise machines. | They built the containment before the capability was public. The org tier is not an afterthought. |
diff |
/diff: the session’s uncommitted changes in a pane beside the transcript, refreshed as Claude edits files. |
A first-party user-facing feature implemented as a mod — the strongest signal that this is intended as the real extension path, not a side door. |
telemetry |
Adds $.telemetry (log, mark) so a plugin can record a first-party analytics row. Sends nothing wherever Claude Code’s analytics are off. |
Demonstrates noun composition: one mod extends the $ every mod above it receives. |
“Our intent is to take further extant features as they exist in CC today and migrate them to mod form.”
That is the most load-bearing sentence for anyone deciding how seriously to take this. A vendor dogfooding its own extension API by rebuilding shipped features on it is the strongest available commitment signal — stronger than a roadmap, because it creates internal cost if the API is bad.
A mod that adds a noun to $ owns that
noun’s types in a single declaration file, and “the contract is the only declaration of the
noun… so the implementation cannot drift from what callers read.” A mod calling another’s
noun reads the same file and never copies it. For a plugin
outside the repo, the README says you point your tsconfig at that mod’s types/
folder for now — “once the engine writes the contracts of the plugins a session has
installed, /plugin-types will put them beside claude-code.d.ts and the include
goes away.” That mechanism does not exist yet.
There is also a real test harness:
claude plugin test mods/diff, with a kit providing tier(),
mock.clock/env/store and $.ui.press. A
testing story this developed at early-access stage is unusual and is itself evidence of seriousness.
sec-default, diff, telemetry ship in the binary with published sourcesec-default) was built before the capability went public$ for mods above them without type drift“A mod is just a plugin that uses function hooks, nothing is changing there. Hopefully the ontology is not too confusing.”
So “mod” is not a seventh plugin component alongside skills, agents and MCP servers. It is a name for a plugin that happens to contain a hooks module. Distribution, manifests, marketplaces, install flow — all unchanged. If you already understand plugins, you already understand 80% of mods.
| Claim you will hear | What is actually true |
|---|---|
| “Mods are a new plugin type.” | Renamed A mod is a plugin. The new thing is the hooks module inside it. |
| “Mods let plugins deny tool calls.” | Already shipped Today’s PreToolUse hooks deny tool calls. Mods change how and add ordering, not the denial itself. |
| “Mods let plugins add slash commands / skills / agents.” | Already shipped Plugins do all of this today. |
| “Mods let plugins draw UI.” | Genuinely new Hooking React components, modifying props, wrapping render nodes. Nothing today does this. |
| “Mods give a guaranteed audit log.” | Genuinely new A hook on * seeing every event including other plugins’ $ calls has no equivalent today. |
| “Mods let admins constrain what plugins can do.” | New mechanism, restoring old isolation See Module 4 — capability removal from $ is new; what it protects was previously unreachable anyway. |
| “Mods can orchestrate other agents.” | Unresolved Raised in the thread and not settled — see objection 3 in Module 9. |
$, guaranteed orderingA new extension point creates a gravitational pull toward using it for everything. Resist it. The ladder below runs cheapest and most stable first; go down a rung only when the one above genuinely cannot do the job.
| Rung | Use when | Stability |
|---|---|---|
| CLAUDE.md / settings | You want different default behaviour or context. No code. | Stable |
| Skill | A repeatable procedure you want Claude to follow or invoke by name. | Stable |
| Classic hook | Deterministic allow/deny/notify at a lifecycle point, independent of other handlers. | Stable |
| Subagent | Isolated context with its own tools and prompt for a sub-task. | Stable |
| MCP server | New tools or data from an external system. | Stable |
| Mod | You need to wrap execution, guarantee ordering against other plugins, observe everything, draw UI, or remove a capability from plugins beneath you. | Unstable |
Ten problems. For each, pick the cheapest mechanism that actually solves it.
Ten goals. For each, decide what the architecture has to give you:
Objections 1–3 are quoted from named commenters in the public thread and are their arguments, not mine and not Anthropic’s. Objections 4–6 are the course’s own and are marked . Where the author replied in the thread, his reply is summarised.
“This starts to feel like a closed-source platform trying to expose enough hooks to be infinitely extensible. At some point the extension API becomes almost as complex as the runtime itself. If plugins can intercept tool calls, filesystem access, UI rendering, engine creation, capabilities, and continuation flow, then all of those things effectively become public ABI. Internal refactors now have compatibility implications.”
And on debugging: “With several middleware layers modifying events, replacing execution,
calling next() multiple times, handling cancellation, etc., a bug can come from the core,
plugin ordering, another plugin, capability wrapping, or some interaction between them.” He
proposes an alternative: open-source the runtime, keep a smaller stable plugin API, and let people fork
when they want to change execution semantics.
“I think it is somewhat of a miss if these function hooks/mods only sit in the execution path as deep middleware… Ideally, a Mod could enact the general agent-runtime/orchestration API on its own (invoking messages, creating sub-agents, etc).”
$ object may already permit more than “middleware” suggests — it exposes
nouns a hook can call, not only events it can answer — but nothing in the published material
settles it.This one is a qualified endorsement rather than a rejection, which makes it more useful. Running a
self-hosted fleet against payments infrastructure in PCI-DSS scope, he reports generating change-control
records by reconstructing agent behaviour after the fact from artifacts, and says
“$.on('*') is the difference between inferred evidence and an actual audit
log. That alone would justify the feature for anyone operating under an audit regime.”
He also notes a usage-limit watchdog that scans terminal panes every five minutes with byte-exact string
matching “because there is no event to subscribe to when a session hits a wall… exactly as
brittle as it sounds.”
“The first one registered wraps the rest” is clean for two parties. With a dozen third-party mods installed, correctness now depends on install order, and no plugin author can know what else is in the stack. Two mods that each work perfectly can combine into a broken session, and the failure will present as a bug in whichever one is easiest to blame.
Consider the demo where a plugin “replaces secrets in tool output before the model reads them,” and the one that hides values in the desktop app until you hover. Both are genuinely good. Both describe a mechanism for altering what the model sees and what the human sees, in a stack where the user may not have written most of the layers. The same primitive that redacts a secret can suppress a warning.
sec-default is precisely the answer for
organisations — capability removal from $, seated outermost, failing closed —
and the wildcard hook means an audit mod can see what every other mod did. For an individual installing community mods with no org tier above them, the protection is
“install things you trust,” which is the same answer as every plugin ecosystem and has the
same track record.“May change between releases without notice” against roughly daily releases means anything you build here is code you have agreed to keep repairing, on a schedule someone else sets. For a personal workflow that is a fine trade. For anything a team depends on, you have taken on an unbounded, externally-timed maintenance obligation in exchange for capability you could mostly approximate with documented mechanisms.
| The objection | Why it fails |
|---|---|
| “This is just hooks with extra steps.” | Today’s hooks run in parallel and cannot wrap, replace, observe other plugins, guarantee order, or render. Four of those five have no equivalent. The mechanism is different in kind, not degree. |
| “Anthropic is adding this to lock people in.” | The design goes the other way: source published, a documented primitive, an org tier that limits what the vendor’s own extension path can reach, and a public thread where the author said community response “likely dictates whether this ships.” Objection 1 is the disciplined version of the underlying worry, and it is about architecture rather than motive. |