Claude Mods
Anthropic's proposed TypeScript middleware for Claude Code: how the continuation model works, the proposed API in full, worked examples, and the open questions on availability and cost.
Claude Code's existing hooks can already intercept a tool call before it runs. What they cannot do is own both ends of one operation — change the input, then see and change the result — from a single place. Claude Mods propose TypeScript handlers that wrap the operation instead of sitting beside it.
It has not shipped. Anthropic has committed to shipping “on the scale of weeks”. The API below is proposed, and whether the reported experimental flag works in current public builds is unverified. Here is the whole thing in one place.
What. A TypeScript module whose functions wrap Claude Code's own behaviour, Express-style, instead of a shell script reacting after the fact. Product name: Claude Mods. Engineering primitive: function hooks.
Status. Not shipped. Proposed in issue #91870 on 3 September 2026, behind CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1. Anthropic has committed to shipping "on the scale of weeks".
Why it matters. The extension point moves from reacting to an event to wrapping the operation. A shell hook sits beside the operation. A function hook wraps it, owning both the input and the result.
The catch. Sequential waiting dominates. A serial chain of eight 300 ms tasks was reported at 2,427 ms; a separate parallel command-hook test of eight 50–300 ms tasks came in around 400–640 ms. Different workloads, so not a matched comparison — but the shape matters.
We're exploring a new way to let you extend and customize Claude Code: Function Hooks.
— ClaudeDevs (@ClaudeDevs) September 3, 2026
Here's a couple videos showing what you'd be able to do. It hasn't shipped yet, we'd love feedback on this on our GitHub issue. pic.twitter.com/0F3kBO3kjl
Watch the Function Hooks demos on X — if the embed above does not load.
What it is
The product name is Claude Mods. The engineering primitive is function hooks, and the proposal's own definition is blunt: a mod is just a plugin that uses function hooks. Plugins stay the packaging; what changes is the mechanism inside them.
Anthropic engineer Alice Poteat opened anthropics/claude-code#91870 on 3 September 2026, labelled area:hooks, area:plugins and enhancement. Her framing at the time: the response from the community would likely dictate whether it shipped at all. A later update commits to it: “We're now committed to shipping function hooks, on the scale of weeks in lieu of days or months.” There is also an architecture document attached to the thread.
A shipping commitment with an approximate timeframe is not general availability. Keep those apart.
The continuation is the whole design
import type { Register } from "claude-code";
export const register: Register = (on, options) => {
on("tool.call", { tool: "Bash" }, ($, e, next) => {
if (e.command === "rm -rf /") {
return { deny: "Destructive command blocked by hook" };
}
return next(e);
});
};Three arguments:
$ — the engine interface, and the capability boundary. More on it below.
e — a frozen plain-data event, typed and narrowed by the matcher.
next(e) — the continuation. It runs the handlers beneath yours and, eventually, Claude Code's own behaviour.
Because the event is frozen, rewriting means passing a new object into the continuation. Denying means returning a result without calling it at all. And because you call it yourself, you get both sides: modify the event on the way down, inspect and modify the result on the way back.
That is the actual change. Pre-execution interception already existed in the classic hooks. Owning the operation — input and result together, in one place — did not.
Read the example as a mechanism demo, not a policy. It compares one exact string. Extra whitespace, a compound command, or any of a dozen equivalent spellings walk straight past it.
Loading a module
A plugin declares its modules in hooks/hooks.json:
{
"modules": ["./my-hooks.ts"]
}Bun transpiles the module in-process. The existing hook types — command, http, mcp_tool, prompt, agent — keep working alongside it.
One compatibility trap worth knowing before you ship a plugin to a team: older Claude Code versions can drop an entire hooks.json that contains keys they do not recognise. Validate against the versions that will actually consume it.
What you can hook
Twenty events across six areas:
Note the spelling: PreToolUse keeps its existing capitalisation while everything around it is dotted lowercase. The proposal also mentions five placements on a single event, but does not enumerate them, so that part is not yet a migration guide. An agent-completion event has been asked for and is not in the list.
$ is the capability boundary
The sigil is described as non-negotiable and jQuery-inspired, which is the least interesting thing about it. What matters is that every engine interaction goes through one parameterised object, which is what makes side-effect tracking possible.
An engine.create hook can selectively remove nouns, giving an admin coarse control over what downstream plugins can reach. Read that precisely: removing $.fs removes that API entry. It does not sandbox the model's own tools, and it does not by itself rule out reaching the filesystem through $.process.run or $.tool.call. Routing engine calls through one object is what makes tracking them possible; it is not a sandbox.
Example: rewrite npm to pnpm, and say so
on("tool.call", { tool: "Bash" }, async ($, e, next) => {
if (!e.command.startsWith("npm ")) return next(e);
const command = e.command.replace(/^npm /, "pnpm ");
const result = await next({ ...e, command });
if (result.deny !== undefined) return result;
return {
...result,
context: [...(result.context ?? []), `Ran ${command} instead of npm.`],
};
});This is the pattern worth internalising, because it works on both sides of the continuation. Going down, it rewrites the command. Coming back, it passes a denial through untouched, or appends context recording what this handler passed to next — downstream handlers may still change it. One handler owns the substitution and its explanation, which is the thing that is awkward with two independent shell hooks. It is a prefix rewrite, not a package-manager policy: npm and pnpm are not generally interchangeable.
Example: redact secrets before they leave
const secrets = new Map<string, string>();
export const register: Register = (on) => {
on("session.start", async ($, e, next) => {
const saved = (await $.store.get("secrets")) as Record<string, string>;
for (const [id, value] of Object.entries(saved ?? {}))
secrets.set(id, value);
return next(e);
});
on("prompt.submit", ($, e, next) => {
let text = e.text;
for (const [id, value] of secrets)
text = text.replaceAll(value, id);
return next({ ...e, text });
});
};Module state, persistent storage and input rewriting in one file. Load a mapping at session start, swap known values for their identifiers on the way out. It requires a mapping already stored under secrets; it neither discovers nor saves them.
It is often described as bidirectional redaction. It is not — as written it only covers the outbound direction. There is no reverse substitution, nothing covering tool results, and the as Record<string, string> assertion does nothing at runtime. Real secret handling needs more than this. The mechanism is the point.
Example: draw something
on("ui.render", { component: "AbovePrompt" }, async ($, e, next) => {
const t = await $.ui.resolve(e);
return (
<t.Box>
<t.Text>Deploy status</t.Text>
<t.Button key="hide" label="Hide" onPress={() => {}} />
</t.Box>
);
});Needs a .tsx extension and the JSX factory settings, which belong under compilerOptions:
{
"compilerOptions": {
"jsx": "react",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment"
}
}Compiler settings alone do not supply h — the proposal's factory setup does. The rewrite and UI snippets both belong inside the register callback from the first example.
The snippet renders static text behind a button that does nothing, so treat it as a shape rather than a feature. The point is the category: the proposal would let a plugin participate in rendering and interaction inside the session, ask arbitrary questions rather than only permission prompts, and register tools of its own.
The three built-in mods
Source for three is published in the repository's mods directory. Published source is not the same as availability in your build.
sec-default — keeps an organisation's classic hooks, prompt content, managed settings and tool policy isolated from user-installed plugins. It adds no policy of its own. Seated outermost on machines with managed settings and for Team and Enterprise orgs.
diff — provides /diff, a pane beside the transcript showing the session's uncommitted changes by file and hunk, updating live as Claude edits and runs things.
telemetry — adds $.telemetry with log and mark through the engine.create fold, so plugins can write first-party analytics rows. Sends nothing when analytics are off.
That last one quietly proves something useful: the fifteen-noun base is not necessarily the interface your plugin ends up receiving. Mods can add nouns.
Order is behaviour
Earlier registration wraps later registration. The sequence is admin-first plugins, then dependency order, then admin-last plugins, with registration order deciding it inside a single plugin.
It is an onion. The outer handler sees the event first and, when it awaits the continuation, sees the result last. Which means a rewriting handler changes what a later policy handler inspects, and an inner handler's annotation can be rewritten by an outer one. Every handler can be locally obvious while the chain as a whole does something nobody intended. Review the chain, not the handler.
Against the shell hooks you already have
The cost of sequential waits
In-process dispatch is cheap. Waiting is not.
The reported serial chain took 2,427 ms for eight 300 ms tasks. The parallel command-hook figure is a different workload — eight tasks of 50 to 300 ms — so the two are not a matched comparison. What they do show is that sequential waiting dominates dispatch overhead, and that for independent work that genuinely parallelises, the old command hooks remain worth keeping.
The development loop
claude --plugin-dir ./my-plugin
claude --debug
claude plugin validate ./my-pluginThe first watches and reloads on save, the second shows why a hook failed, the third lists the hooks and calls it found. Inside a session:
/plugin-types ./types
/reload-pluginsFailed hooks are skipped, with the reason in the debug log: it threw, it overran its budget, or it returned an unexpected shape. Run with debug on while you are building, or you will spend an afternoon wondering why nothing happens. What a failure does to the operation itself — especially a handler that fails after calling the continuation — is worth establishing before you use these to enforce any policy.
Can you actually use it today?
CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claudeCommunity reports place a working implementation in build 2.1.260. I checked five builds installed on my own machine — 2.1.266, .267, .268, .270 and .271 — and could not find the flag string in any of them. That is weak evidence: these are large bundled binaries and a string search proves very little. It is not evidence of removal. It is a reason to check your own installation rather than assume.
The declarations are explicit about the contract: “EARLY ACCESS: this surface may change between releases without notice.”
Still open
Anthropic has committed to shipping; the final API and the release date have not settled. The exact $ noun set is being designed with partners. next.trace, for per-link timing and snapshots, is proposed but absent from the declarations. Typed errors are described as needing deep thought. Surfaces beyond the CLI and Desktop are “top of mind but tbd”. A /plugin.register hook to control which plugins load is proposed and not implemented.
Worth your time?
Read the proposal, and prototype against it if you maintain anything that fights the current hook system. Continuations are the right call when one handler needs to transform both an operation's input and its result — that is the case the existing hooks handle worst.
Do not put a production workflow on it. It is behind a flag, on a surface that says in writing it may change without notice, in a build you may not have. Prototype once you have a build where it demonstrably works; plan the migration against the released API.