Guide

Claude Code mods, explained: what they are, what they can do, and what hooks never could

A Claude Code mod is a small JavaScript or TypeScript function that runs inside Claude Code and is called every time something happens: a tool call, a submitted prompt, a turn ending, a part of the screen being drawn. It can watch the event, rewrite it, or take it over, and it can draw its own panes, bands and buttons in the terminal. Mods shipped in Claude Code 2.1.287 on 1 October 2026, they are on by default, and /diff is already one. This guide covers what a mod is, how to install one, a three-file example that was validated and run on 2.1.287 while writing this page, how mods differ from hooks, skills and MCP servers, where they render, and the trust model you accept when you install one.

Applies to Claude Code 2.1.287 or later · checked on 2 October 2026 · the mods API is early access and can change between releases

The short version

  • A mod is a plugin with code in it. Any Claude Code plugin whose hooks/hooks.json has a modules key naming a .js or .ts file is a mod. Three files, no build step, no Node.
  • It runs in-process. Claude Code loads the module into a worker of its own and calls its functions on its events. A settings hook is a separate shell command per event; a mod is loaded once and stays.
  • It can do three things with an event: observe it and let it continue, rewrite it before it continues, or answer it so Claude Code's own behaviour never runs.
  • It can draw. A pane beside the transcript, a band above the prompt, a status line entry, toasts, log lines, and it can redraw rows Claude Code draws itself, such as a tool call or the spinner.
  • It installs like any plugin, from a marketplace, with /plugin install name@marketplace, or for one session with claude --plugin-dir ./folder.
  • It runs as you, unsandboxed. Install only from people you trust, and run claude plugin validate first to see exactly what it hooks and calls.
  • The API is early access. Anthropic says events and methods can change between releases, and that the type declarations Claude Code writes next to your mod are the authority for your build.

Sources for everything on this page are Anthropic's mods documentation, the launch post, the getting-started guide by Addy Osmani, the changelog entry for 2.1.287, and a mod built and run on this machine. Where a number appears, the date it was read is next to it.

What a Claude Code mod actually is

Anthropic's own definition: "A mod is a plugin that changes how Claude Code looks and behaves. It's made of JavaScript or TypeScript event handlers: Claude Code calls one when an event happens, such as a tool call, a submitted prompt, or a part of the interface being drawn, and the handler can watch the event, change it, or take it over."

The word "hook" is overloaded here, and the docs say so. Claude Code calls both kinds hooks. On the mods pages, "hook" means a mod's handler function, and the older settings-file kind is a "settings hook". The launch post is blunt about why mods exist: "Hooks helped give users some of this control, but hooks can't rewrite events, draw new UI, or replace features. Mods can."

Every handler has the same shape, three arguments:

on('tool.call', { tool: 'Bash' }, async ($, e, next) => { // $ the mods API: ui, session, state, store, fs, process, clock, http, tool, command, model ... // e this event's input, as plain frozen data // next passes e on to the other mods and then to Claude Code's own behaviour return next(e) })

Hooks form a chain, like middleware. The first mod to load sees the event first and the result last, so mods from different authors stack. What a hook does with next is the whole design:

MoveHowExample
Observeconst r = await next(e), then look at rCount tool calls. Record every file edit. Take a usage reading after each turn.
Rewritereturn next({ ...e, command: safer })Change what the rest of the chain, and Claude Code, sees.
Answerreturn { deny: '...' } without calling nextRefuse a tool call. Serve a slash command yourself. Draw your own tree instead of Claude Code's.

Anthropic uses the mechanism itself. Run /plugin and look under Built-in on the Installed tab: cc-plugin-diff is the /diff pane, cc-plugin-agents-md is what reads AGENTS.md as project instructions, cc-plugin-sec-default is the policy guard described below, cc-plugin-telemetry sends the analytics records, and cc-plugin-you-should-know is an off-by-default side agent that leaves notes above the prompt. The source of diff, agents-md, sec-default and telemetry is public in the mods directory of the Claude Code repository, each a complete plugin with tests. The launch post says more built-in features will move to mods over time, "so you can pare Claude Code down to a small core and add back only what you want".

What a mod can do that nothing else could

Settings hooks, skills, status lines and MCP servers all work from outside Claude Code: each one runs a script or gives Claude text or tools. A mod runs inside, which is what unlocks the list below. Each item links to the page in the docs that covers it.

  • Draw an interface you can use. A pane is a sidebar beside the transcript in a wide fullscreen terminal, or a framed region above the prompt otherwise; with several open, each gets a tab. The band is a strip directly above the prompt input that is always there and every mod shares. Panes and bands hold Box, Text, Button, Input, Select, Link, Code (including diffs) and Markdown, plus Svg on the Desktop app and Raster and Image in the terminal. (Draw in the interface, gallery)
  • Redraw Claude Code's own interface. A ui.render hook can replace or restyle a user message, an assistant message, a tool call row, a tool result, the spinner, the turn duration line, the AskUserQuestion dialog and more. The overview's own first example adds a tool-call count to the spinner's word in two hooks.
  • Step into a tool call or a request. Hold a tool call while you ask the user something, answer it without running the tool, rewrite its arguments, or send a request to a different model. (React to events)
  • Run your own code on a command. $.command.register adds a /command that runs your function at once, with no Claude turn, even while Claude is working. A mod can also register a tool the model can call, which shows up as mcp__<plugin>__<name>. (Use the mods API)
  • Keep state and share it. A mod's hooks share the variables in their file. $.state holds per-session values that survive a hot reload and redraw whatever reads them. $.store is a key-value store that every session on the machine shares.
  • Reach outside. $.fs reads and writes files, $.process.run and $.process.spawn start programs, $.http.fetch makes requests, $.clock runs timers that outlive a dispatch, $.model.complete and $.model.fork call a model on your plan, and $.session.send messages another of your sessions.
  • Read the numbers Claude Code has. $.session.usage() returns the context window's tokens, size and fill percent, the rate-limit windows with their reset times, and cost. The session.measure event fires after each turn and whenever a plan limit's percentage changes. This is why so many of the first mods are usage meters: they read the number directly instead of parsing a transcript.

The one thing it cannot touch is worth stating early, because people ask: "The permission prompt isn't a render site, so a mod can't change what it shows." A mod can decide a call before the prompt appears, and can add one line of context under an open dialog, but it cannot redraw the prompt itself.

Where mods run, and where they draw

Hooks run in every kind of session that loads the plugin. Drawing is narrower. This is Anthropic's table, condensed:

Where you run Claude CodeHooks runWhat the mod draws appears
Terminal, including editor terminals and the JetBrains pluginYesYes
Code tab of the Claude Desktop appYesYes, except terminal-only elements
Desktop app in a WSL sessionNo, plugins are unavailable thereNo
VS Code extension's chat panelYesNo
claude -p and the Agent SDKYesNo
Remote Control from claude.ai or the phoneYes, on your machineIn the terminal on your machine
Cloud sessionYes, if the plugin reaches the cloud sessionNo

A mod can check which surface it is on (e.surface is terminal or desktop in a ui.render hook) and fall back to a log line or a command's text reply where nothing draws. Two more rules shape what you see: a pane a mod opens by itself, from a timer or a turn.start hook, appears only in a terminal at least 144 columns wide, while a pane you opened with a command or button appears at any width; and a mod's status line entry always starts with ⚠ and the mod's name.

Mods vs hooks vs skills vs MCP vs the status line

The question behind "claude code mod vs hook" is really "which one do I reach for". The table is Anthropic's comparison with the status line added, since mods make a status line entry too.

ModSettings hookSkillMCP serverStatus line
What it isFunctions in a plugin, called in Claude Code's own processA shell command, HTTP request or prompt run on a lifecycle eventA SKILL.md of instructions Claude readsAn external process that gives Claude toolsA script whose output fills the line under the prompt
Can changeTool calls, prompts, commands, turns, and what the interface drawsWhether a tool call or prompt goes ahead, its arguments and result, context for ClaudeWhat Claude knows and doesWhich tools Claude hasOne line of text
Draws UIYesNoNoNoText only
You writeJavaScript or TypeScriptA script and a settings.json entryMarkdownA server in any languageA script
Pick it whenYou want a pane, a band, a custom command, or to rewrite an eventYou want to block, allow or log with a script you already haveYou keep pasting the same instructionsClaude needs an external systemYou want one line of ambient info

Three things about the relationship between mods and settings hooks matter in practice.

Settings hooks are not going away. The admin page says it outright: "Nothing about them is deprecated." Every settings-hook event is also a mod event, named classic.<Event>, with the same JSON the settings hook would get on stdin: classic.PreToolUse, classic.PermissionRequest, classic.Stop and so on. A mod that handles classic.PermissionRequest can answer it with a decision, exactly as a settings hook prints one.

Mods run before most settings hooks. The order is managed settings hooks first, then the mods, then the other settings hooks. A mod that returns its own result for a tool call instead of calling next therefore keeps your PreToolUse hooks from running at all. That is how a mod can approve a call your own hook would have blocked, which is one of the reasons the trust section below exists.

A plugin can hold all of them. A mod can ship in the same plugin as a skill, slash commands, subagents and an MCP server. If you already have a hook script that blocks or logs, you do not need a mod. If you want the result to show up as something you can look at and press, you do.

If you want the hook side of this in detail, the hooks guide covers the lifecycle, the config shape and a first hook.

How to install a Claude Code mod

A mod installs as a plugin, from a marketplace. A marketplace is a GitHub repository or a local directory with a .claude-plugin/marketplace.json in it, so every author who publishes a mod publishes a marketplace with it. First check your version:

claude --version # 2.1.287 (Claude Code) or later. Older versions ignore the modules key.

Then, inside a session, add the marketplace, install the plugin, and reload. This installs What's Agent Doing, the "what is Claude doing right now" box from the best-mods list:

/plugin marketplace add tzafrir/whats-agent-doing /plugin install whats-agent-doing@tzafrir /reload-plugins

The same three steps work from your shell as claude plugin marketplace add and claude plugin install, and a mod installed from the shell loads the next time you start Claude Code, or when you run /reload-plugins in an open session. The @ part is the marketplace's name field, which is not always the repository name. Anthropic's own community marketplace is anthropics/claude-plugins-community on GitHub but claude-community after the @.

To try a mod for one session without installing it, point Claude Code at the directory:

claude --plugin-dir ./first-mod

Claude Code watches a --plugin-dir directory and hot-reloads the module when a file in it changes, which is also how you develop one. Three things to know after installing:

  • See what loaded. Run /plugin. A dim line under the tabs reads, for example, 1 mod active · whats-agent-doing. Built-in mods are left out of that count.
  • Updates are cached by version. Claude Code caches an installed plugin by its version, so an author's edits do not reach you until they raise the version and you update. Auto-update is on for Anthropic's marketplaces and off by default for every other one.
  • Old READMEs still mention a flag. Mods were in public early access for about four weeks before launch behind CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1, and many repositories still tell you to set it. Claude Code 2.1.287 and later ignores that variable; mods are on by default, and setting it to 0 does not turn them off. If a README says "function hooks" or "early access flag", the mod predates launch, and that is all it means.

Anthropic's directory at claude.ai also lists plugins that include mods, and the Desktop app's Code tab draws mods once the app bundles 2.1.287 or later. If a mod loads in your terminal and not in the app, compare the two versions before suspecting the mod.

A minimal mod, tested on 2.1.287

This is the whole of a working mod: it shows the current git branch and the turn's tool-call count in the band above the prompt. It was validated, unit-tested with claude plugin test, and run in a terminal session on Claude Code 2.1.287 while writing this guide. Three files, no build step.

branch-band/.claude-plugin/plugin.json, the plugin manifest:

{ "name": "branch-band", "version": "0.1.0", "description": "Shows the current git branch and the turn's tool-call count above the prompt", "author": { "name": "Your Name" } }

branch-band/hooks/hooks.json, which is what makes the plugin a mod. The modules key names one file, relative to this one:

{ "modules": ["./register.js"] }

branch-band/hooks/register.js, the hooks module. It exports register, which Claude Code calls once with on:

// Shared by every hook below; a reload starts them over let branch = '?' let calls = 0 export function register(on) { // When the session starts, read the branch once through the mods API on('session.start', async ($, e, next) => { const r = await $.process.run(['git', 'rev-parse', '--abbrev-ref', 'HEAD']) branch = r.exitCode === 0 ? r.stdout.trim() : 'no git' return next(e) }) // Count tool calls, then let each one run as usual on('tool.call', async ($, e, next) => { calls += 1 $.ui.invalidate('ui.render') return next(e) }) // Draw one line in the band above the prompt on('ui.render', { component: 'AbovePrompt' }, async ($, e) => { const { Box, Text } = $.ui.resolve(e) return Box({ children: [ Text({ bold: true, children: [branch] }), Text({ dimColor: true, children: [' · tool calls: ' + calls] }), ] }) }) }

The three hooks are the three moves. session.start and tool.call observe: they do their work and return next(e), so the session starts and the tool runs as usual. The ui.render hook answers: it returns its own tree and never calls next, so Claude Code draws that instead of an empty band. $.ui.resolve(e) hands back the elements the current surface can draw, and $.ui.invalidate asks for a redraw after the count changes.

Before running it, see the mod the way Claude Code sees it. This is the real output on 2.1.287:

$ claude plugin validate ./branch-band ❯ ./register.js hooks: session.start, tool.call, ui.render{component=AbovePrompt} ❯ ./register.js calls: $.process.run, $.ui.invalidate, $.ui.resolve ✔ Validation passed

Those two lines are the point of validation. Claude Code reads the module's source statically, so the hooks: line is exactly the events that will fire and the calls: line is exactly what the mod can reach. A misspelled event name is an error, not a silent no-op. For that to work you follow a few rules: write every API call in full as $.namespace.method, write event names as string literals, use import declarations rather than dynamic import(), and import only from files inside the plugin. The one bare import allowed is claude-code, for types and the test helpers.

Then run it for one session:

claude --plugin-dir ./branch-band

In a repository the band reads main · tool calls: 0 above the prompt and the count rises as Claude works; in a folder with no repository it reads no git. /plugin shows 1 mod active · branch-band. Edit the file while the session is open and a line in the transcript says the module reloaded, which is the loop you develop in.

A mod can also be tested with no session, sign-in or network. A test fires the events your hooks handle and checks what they did. This one stubs the git call, fires one tool call, mounts the band and checks both texts:

// branch-band/tests/branch-band.test.ts import { expect, test } from 'claude-code/testing' test('the band shows the branch and the tool-call count', async ($, on) => { on('session.start', () => ({ cwd: '/work' })) on('process.run', () => ({ value: { exitCode: 0, stdout: 'main\n', stderr: '' } })) on('tool.call', () => ({ result: 'ok' })) await $.session.start({ surface: 'terminal', isInteractive: true, cwd: '/work' }) await $.tool.call({ tool: 'Bash', command: 'ls' }) const ui = await $.ui.mount({ plugin: 'branch-band', component: 'AbovePrompt', surface: 'terminal', viewport: { columns: 100, rows: 30 }, props: {} }) expect(await ui.find({ type: 'Text', text: 'main' })).toBeDefined() expect(await ui.find({ type: 'Text', text: / tool calls: 1$/ })).toBeDefined() await ui.unmount() })
$ claude plugin test tests/branch-band.test.ts: (pass) the band shows the branch and the tool-call count [21.58ms] 1 pass 0 fail

The first version of that test called $.session.start() with no event and failed with a message about next() taking the event's argument. The docs' rule is that every stub is registered before the test's first call on $, and a stub for session.start itself is needed if your hook calls next. That is representative of what writing a mod is like right now: the error messages are specific, and the test page has the answer, but the API has edges you learn by hitting them.

The easier route is to not write it at all. In an interactive session, describe the mod you want, such as "make a mod that shows the current git branch above the prompt". Claude loads its built-in plugin-authoring skill, writes the files into ~/.claude/dev-mods/<session id>/, and Claude Code asks once whether to enable hot reloading for the session. Say yes and the mod loads when the turn ends and reloads after each turn that changes it. A mod written that way lives only in that session and is cleaned up with it, so copy the directory somewhere of your own to keep it.

The events you will actually hook

The reference lists more than forty events in the mod's own namespace plus a classic.* mirror of every settings-hook event. The ones nearly every published mod uses:

EventFires whenWhat a hook can do
tool.callClaude is about to use a tool; a matcher narrows it to one toolDeny it, rewrite its input, answer it with your own result, or await the result
tool.checkClaude Code decides whether a call needs permissionReturn allow, ask or deny; carries the call's tool_use_id
prompt.submitYou send a promptRewrite the text, add to it, or hold it
turn.start, turn.step, turn.completeA turn starts, streams, endsFollow what Claude is doing live; turn.complete carries the answer, duration, isAborted and usage
session.start, session.endThe session starts (and the mod reloads), the session closesRegister commands, read config, write a final line; session.end hooks share 1.5 seconds
session.measureAfter each turn and whenever a plan limit's percent changesRead context fill and rate-limit windows without polling
command.runA /command your mod registered is typedRun your function and return text, with no Claude turn
ui.renderClaude Code is about to draw a render siteReturn your own tree for a pane, the band, the spinner, a tool row and more
agent.spawnA subagent startsTrack or gate subagents
classic.PreToolUse, classic.PermissionRequest, classic.Stop ...The matching settings-hook eventSame JSON a settings hook would get, same answers

Every mods API call is itself an event too, so a mod that loads first can see what other mods do: that is how the built-in guard and an organisation's own policy mod work.

Limits

Hooks run under budgets, and Claude Code skips a hook that exceeds one. From the reference, as of 2.1.287:

  • A hook gets 10 seconds of its own running time per event. The clock stops while a next(e) call or any mods API call is in flight, which is how a mod can hold a tool call for as long as it takes you to press a button.
  • All session.end hooks together get 1.5 seconds.
  • $.process.run times out at 30 seconds by default and 10 minutes at most.
  • $.fs.read and $.fs.write handle 4 MiB per file; $.store holds 4 MiB of JSON in total; $.session.messages() returns the newest 4,096 entries.
  • A toast shows for 4 seconds unless you pass a timeout.
  • No Node APIs, no DOM, no setTimeout (use $.clock), no dynamic import().

Security and trust: what you accept when you install a mod

Anthropic's warning sits at the top of the install section, and it should sit at the top of yours: "A mod is code that runs with your permissions. It can read and write your files, start processes, and make network requests. Install mods only from authors and marketplaces you trust." In full, the overview says a loaded mod can:

  • act on your machine as you: read and write any file your account can, start programs, make network requests;
  • read your secrets, including an API key in an environment variable or a settings file;
  • see every prompt you send and every tool call Claude makes;
  • change your session: rewrite a prompt or a tool call, submit a prompt as if you typed it, message another of your sessions;
  • act without asking you, by approving a tool call before you are asked;
  • spend your usage, by calling a model on your plan or API key.

Mods are not sandboxed. If you have sandboxing on, it isolates the Bash commands Claude runs; a process a mod starts runs outside it. Installing the plugin is the approval. There is no per-capability permission prompt, which is why the static claude plugin validate listing exists: clone the repository, run it on the directory, and read the hooks: and calls: lines before you install. A usage meter should list $.session.usage and $.ui.*; it has no reason to list $.http.fetch or $.process.spawn.

Four more mechanisms shape what a mod can do once it is in.

The built-in guard. Claude Code loads a mod named sec-default@builtin ahead of every mod a user installs, and users cannot turn it off. It loads when the machine has managed settings or the user is signed in on a Team or Enterprise plan. Where it loads, a user's mod cannot approve a call that a deny rule refuses, a block from a managed PreToolUse hook is final, and the guard keeps the settings-hook events (classic.*) away from mods a user installed. Administrators can additionally set allowManagedModsOnly so that only the organisation's own mods load, or load a policy mod of their own that watches every call every other mod makes. The guard's source is public.

What the guard does not cover. Outside it, a user's mod can approve a call that an ask rule would prompt for, or that one of your own PreToolUse hooks blocked. And deny rules apply to Claude's tool calls, not to the mod's own calls: with Read(.env) denied, a mod can still read that file through $.fs.read.

A remote off switch. The troubleshooting page lists the debug-log line hooks modules are turned off in this process and explains it: "Anthropic has turned installed mods off remotely. No setting on your machine turns them back on." Built-in mods are not affected. Anything you build on mods should be able to survive that.

Shared fate. Installed mods share one worker thread. A mod that blocks the thread, with a loop that never awaits for example, is traced and unloaded on its own. If the worker crashes three times and Claude Code cannot trace the crashes to one mod, every mod that is not built in is turned off for the session.

To turn mods off yourself: disable or uninstall one from the Installed tab in /plugin; start with --safe-mode to stop every installed mod for one session; or set "disableAllHooks": true in ~/.claude/settings.json to stop them everywhere, which also stops your settings hooks and custom status line. --bare stops them too. None of these stop the built-in mods.

Where a notch app fits next to a mod

Everything a mod draws lives inside one Claude Code session, in the terminal or Code tab that session is running in. That is exactly the right place for a context forecast, a plan tracker or a guard on rm -rf. It is the wrong place for the moment you are in another window and one of several sessions, or a Codex or Cursor session that has no mods at all, is waiting on you.

That is the gap CrewTower covers: a macOS app that puts every running agent session in the MacBook notch, across Claude Code, Codex, Cursor, Gemini CLI, Qwen Code and opencode, and lets you approve, deny or answer from there. It talks to Claude Code through settings hooks and a local socket, the same mechanism the permission guide describes, and it ships no mod. The two are complementary: a mod makes the session you are looking at richer, and the notch tells you about the ones you are not.

Questions

Are Claude Code mods the same thing as hooks?

No. A settings hook is a shell command, HTTP request or prompt that Claude Code runs on a lifecycle event, passing JSON over stdin and stdout. A mod is a JavaScript or TypeScript function that Claude Code loads into its own process and calls on more than forty events, including every part of the interface it draws. A hook can allow, block or log an event. A mod can also rewrite it, answer it in Claude Code's place, draw panes, bands, buttons and text fields, register slash commands, and keep state across the session. Settings hooks keep working and are not deprecated.

Do I need Node.js or a build step to write a Claude Code mod?

No. Claude Code loads .js, .mjs, .ts and .tsx files directly. A mod is three files: a plugin manifest, a hooks.json that names the module, and the module itself. The module runs in its own environment with no DOM and no Node, so everything outside it goes through the mods API object named $.

Where do Claude Code mods work?

Hooks run in every session that loads the plugin, but drawing is narrower. A mod's panes, bands and redrawn rows appear in the terminal, including editor terminals and the JetBrains plugin, and in the Code tab of the Claude Desktop app. They do not appear in the VS Code extension's chat panel, in claude -p or the Agent SDK, in cloud sessions, or in a Desktop WSL session, where plugins are not available at all.

Can a mod change the permission prompt?

No. The permission prompt is not a render site, so a mod cannot change what it shows. A mod can decide a tool call before the prompt appears, through the tool.call and tool.check events, and it can add a single line of context under an open dialog. The AskUserQuestion dialog is a render site, so a mod can redraw that.

Are Claude Code mods safe to install?

A mod is code that runs with your permissions and is not sandboxed. It can read and write your files, start processes, make network requests, read your environment variables, see every prompt and tool call, and approve a tool call before you are asked. Anthropic's advice is to install mods only from authors and marketplaces you trust. Before installing, run claude plugin validate on the plugin directory: it lists every event the mod hooks and every mods API call it makes, without running it.

How do I turn Claude Code mods off?

Disable or uninstall one mod from the Installed tab in /plugin. Start Claude Code with --safe-mode to stop every installed mod for one session. Set disableAllHooks to true in ~/.claude/settings.json to stop them in every session, which also stops settings hooks and a custom status line. Organisations can stop user-installed mods through managed settings, and Anthropic can turn installed mods off remotely.

Is the Claude Code mods API stable?

No. The mods API is early access and the events and methods can change between releases. Claude Code writes type declarations for the exact version you run into the mod's .claude-plugin/types/ directory each time it loads the mod, and Anthropic says to trust those files over any documentation page when they disagree.

How do I see which mods a session has loaded?

Run /plugin at the Claude Code prompt. A dim line under the tabs gives the count and the names, such as 1 mod active · first-mod. Built-in mods are listed under Built-in on the Installed tab and are left out of that count.

Every session, not just the one you are looking at

A mod lives inside one terminal. CrewTower puts all of your agent sessions in the MacBook notch and lets you approve, deny or answer from there, Claude Code and Codex alike.

Get CrewTower for Mac

$9.99 once · macOS 15 or later