ByteVancer
AI Engineering

Claude Code Mods: Rewrite the Agent From the Inside

Claude Code mods explained: in-process JS/TS hooks that draw panes and rewrite tool calls. Your first mod, guard rails, security and team controls.

By · Software Engineer · Published · 11 min read

Claude Code just got a mod menu. On October 1, 2026, @ClaudeDevs posted "You can now mod Claude Code" (around 2.6M views at the time we checked): change how it behaves, customize the UI, swap in your own features, with a few lines of TypeScript, or by having Claude build the mod for you. Mods ship inside plugins, so you install them with /plugin in the CLI or the desktop app. We read the new mods docs and turned them into diagrams, verbatim code and a security checklist. Facts are Anthropic's unless we say otherwise; the jokes are ours.

TL;DR

  • A mod is a plugin made of JavaScript or TypeScript event handlers that run inside Claude Code, not as an outside script.
  • Each handler can observe an event, rewrite it, or answer it outright (so the tool never runs).
  • Mods can draw: a pane, a band above the prompt, a status line, toasts, and restyled built-in pieces like the spinner. Not the permission prompt.
  • Needs Claude Code v2.1.287 or later, where mods are on by default. Review any mod with claude plugin validate before you install it.
  • Mods are not sandboxed. A mod runs with your permissions and can approve a tool call before you're asked. Install like you'd run a stranger's script: carefully. 🫡

What dropped

The requirement is short: Claude Code v2.1.287 or later, and mods are on by default. If you were in early access, the CLAUDE_CODE_ENABLE_FUNCTION_HOOKS flag is now ignored. Mods work in the CLI and in the Code tab of Claude Desktop. Installing one looks like any other plugin:

From Anthropic's mods overview.
/plugin install token-chart@your-org
# or, from your shell
claude plugin install token-chart@your-org
# installed from the shell mid-session? then run
/reload-plugins

Once something is loaded, /plugin shows a dim line like 1 mod active · first-mod. Runtime Wire's Ryan Merket flagged the headline risk on launch day: mods are unsandboxed and run with full machine privileges. More on that below.

What a mod actually is

Anthropic's one-liner: "A mod is a plugin that changes how Claude Code looks and behaves." Under the hood it's a set of event handlers. When something happens (a tool call, a submitted prompt, part of the interface being drawn), Claude Code calls your handler, and your handler can watch, change, or take over. The big difference from the hooks you already know: settings hooks (a shell command, HTTP call or prompt in your settings file) run outside Claude Code. A mod's handlers are functions that run inside it.

One event through the mod chain
  1. 01

    Event fires

    e.g. tool.call: Claude is about to run Bash.

  2. 02

    Your hook runs

    It gets ($, e, next): the mods API, the event, and the rest of the chain.

  3. 03

    Pick a move

    Observe: return next(e). Rewrite: call next with a changed copy. Answer: return a result without next.

  4. 04

    next() passes it on

    Guard and org mods first, then mods you install, then appended and built-in mods.

  5. 05

    Claude Code acts

    Unless a hook answered first, in which case the tool never runs.

One more rule makes mods reviewable: to do anything outside its own code (draw, add a command, call a model, read a file, start a process, hit the network), a mod has to go through the mods API, $. That's why Claude Code can list what a mod does before you install it. In harness engineering terms, a mod is harness customisation that runs inside the agent itself; our harness vs loop vs graph breakdown helps you pick which layer to mod first.

Your first mod: count tool calls

The docs' starter mod adds a /tally command and shows a live tool-call count in the spinner. No Node.js install, no bundler, no build step: Claude Code loads .js and .ts files directly. Three files:

File layout, from Anthropic's "Create a mod" page.
first-mod/
├── .claude-plugin/
│   └── plugin.json
└── hooks/
    ├── hooks.json
    └── register.js
hooks/hooks.json: points Claude Code at the module.
{
  "description": "The first-mod hooks module",
  "modules": ["./register.js"]
}
hooks/register.js, verbatim from Anthropic's "Create a mod" docs.
// The count, shared by the hooks below
let calls = 0

// Claude Code calls this once when the mod loads
export function register(on) {
  // Runs when the session starts, before your first prompt
  on('session.start', async ($, e, next) => {
    // Add the /tally command
    await $.command.register({
      name: 'tally',
      description: 'Show how many tool calls Claude has made',
    })
    // Let the session start as usual
    return next(e)
  })

  // Runs each time Claude is about to use a tool
  on('tool.call', async ($, e, next) => {
    calls += 1
    // Ask Claude Code to draw the interface again, so the new count shows
    $.ui.invalidate('ui.render')
    // Let the tool run as usual
    return next(e)
  })

  // Runs when you type /tally, and only then, because of the matcher
  on('command.run', { command: 'tally' }, async () => {
    // The text to print in the transcript
    return { text: 'Claude has made ' + calls + ' tool calls since this mod loaded' }
  })

  // Runs each time Claude Code draws the spinner
  on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
    // Keep Claude Code's spinner, with the count added after its word
    return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
  })
}

Load it with claude --plugin-dir ./first-mod. The spinner now reads like Thinking · tool calls: 2…, and /tally prints first-mod: Claude has made 2 tool calls since this mod loaded. It hot-reloads on save (each reload resets the count to 0, because module variables are lost on reload). Notice the module variable calls: that's how hooks share data with each other.

first-mod, live

  1. 01SPINNER

    Thinking · tool calls: 1…

    Claude reaches for its first tool.

  2. 02SPINNER

    Thinking · tool calls: 2…

    tool.call fires, the count goes up, the UI redraws.

  3. 03SPINNER

    Thinking · tool calls: 3…

    Still Claude's spinner, just with a suffix.

  4. 04/tally

    first-mod: Claude has made 3 tool calls since this mod loaded

    No Claude turn needed. Just your code. 😌

Dramatised from the docs' first-mod example (the docs show 2 tool calls; we let it run to 3).

Before you trust any mod, including this one, run the validator. It statically lists every hook and every API call:

Output of claude plugin validate ./first-mod, per the docs.
  ❯ ./register.js hooks: session.start, tool.call, command.run{command=tally}, ui.render{component=Spinner}
  ❯ ./register.js calls: $.command.register, $.ui.invalidate

✔ Validation passed

To keep that analysis honest, write every $ call in full and event names as string literals. Claude Code also writes .d.ts types into .claude-plugin/types/ (the docs say trust those over any page), and claude plugin test runs *.test.ts files with no session, sign-in or network.

Or let Claude write it

Too lazy for three files? Valid. Claude Code has a built-in plugin-authoring skill. Ask something like "make a mod that shows the current git branch above the prompt" and Claude writes it to ~/.claude/dev-mods/<session-id>/<name>/, then Claude Code asks whether to enable hot reloading for this session ("Enable for this session" or "Not now"). Mods Claude writes load only in that session; to keep one, copy it out and run it with claude --plugin-dir ~/mods/git-branch.

Where mods can draw

This is the part no settings hook can touch. A mod gets UI elements (Box, Text, Button, Link, Code, Markdown, Input, Select and more) from $.ui.resolve(e), or writes JSX in .tsx/.jsx files. Here's the screen map from the interface docs:

Render sites a mod can draw in (per Anthropic's interface docs)

Your mod

Draws through $.ui; every pixel goes through the API.

  • Pane

    A sidebar on the right in a wide fullscreen terminal; a framed region above the prompt otherwise.

  • AbovePrompt band

    A band above the prompt, shared by every mod.

  • Status · toast · log

    $.ui.status line under the prompt, $.ui.toast top right, $.ui.log as a dim transcript line Claude doesn't read.

  • Built-in sites

    Spinner, ToolUse, ToolResult, messages, AskUserQuestion, PromptHint, TurnDuration and more.

Off-limits: the permission prompt

It is not a render site. A mod can restyle much of the UI, but not that.

State lives in a module variable (lost on reload), $.state (per session, reactive) or $.store (persists, 4 MiB). Gotcha: a pane a mod opens by itself only shows in a terminal at least 144 columns wide (110 once you've opened it yourself).

Guard rails you can build

The events docs ship three guard patterns. All verbatim, all on the tool.call / tool.check events. First, the hard no:

Deny force pushes: the hook answers the event, so Bash never runs. From Anthropic's events docs.
// The matcher limits the hook to Bash calls, so e.command is the shell command
on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
  if (/git push .*--force/.test(e.command)) {
    // Returning without calling next answers the event, so the command never runs
    return { deny: 'Force pushes are not allowed in this repository. Push to a new branch instead.' }
  }
  // Every other command goes on to the permission check and then to Bash
  return next(e)
})

Second, hold the call and ask a human. The tool call literally waits on your answer:

Hold risky commands with $.ui.ask, from Anthropic's events docs. Note: this RISKY regex misses forms like git push -f.
const RISKY = /\brm\s+-rf?\b|\bgit\s+reset\s+--hard\b|\bgit\s+push\b.*--force/

export function register(on) {
  on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
    // Let every other command through without a question
    if (!RISKY.test(e.command)) return next(e)
    // Start from the safe answer, so a question nobody answers refuses the command
    let answer = 'Refuse'
    try {
      // The tool call waits here until the user picks one of the two labels
      answer = await $.ui.ask('Run this command? ' + e.command, ['Run it', 'Refuse'])
    } catch {
      // The user dismissed the question, or this is a claude -p run with nobody to ask
    }
    if (answer !== 'Run it') {
      // Answer without calling next, so the command doesn't run
      return { deny: 'The user declined this command. Ask before trying a different approach.' }
    }
    return next(e)
  })
}

Third, tool.check: change what the permission check decided. This one denies git push while you're on main:

From Anthropic's events docs.
on('tool.check', { tool: 'Bash' }, async ($, e, next) => {
  // What the permission rules and settings hooks decided: 'allow', 'ask', or 'deny'
  const decided = await next(e)
  if (!e.input.command.includes('git push')) return decided
  const branch = await $.process.run(['git', 'branch', '--show-current'])
  if (branch.stdout.trim() !== 'main') return decided
  return { decision: 'deny', reason: 'Push from a branch other than main' }
})

The same page also has a prompt.submit hook that adds your git branch to context when a prompt mentions a PR, and a turn.step hook that logs prompt-cache usage (turn.step can even route a request to a different model). One catch: a hook that throws is skipped (fails open) unless you add .catch(...). For a guard, fail closed.

Mods vs settings hooks vs skills vs MCP

ModSettings hookSkillMCP server
What it isFunctions in a plugin that Claude Code calls in its own processShell command, HTTP call or prompt run on a lifecycle eventSKILL.md instructions Claude readsExternal process that gives Claude tools
What it can changeTool calls, prompts, commands, turns, what the interface drawsWhether a tool call or prompt goes ahead, args and results, contextWhat Claude knows and doesWhich tools Claude has
Draws in the interfaceYesNoNoNo
You writeJS or TSA script + a settings.json entryMarkdownA server in any language
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 system
From the comparison table in Anthropic's mods overview. A single plugin can hold all four.

Where mods run

WhereHooks runDrawing appears
claude in a terminal (incl. editor terminals, JetBrains plugin)YesYes
Claude Desktop Code tab (not WSL sessions)YesYes, except terminal-only elements
WSL session in DesktopNo (plugins not available)No
VS Code extension chat panelYesNo
claude -p and the Agent SDKYesNo
Remote Control from claude.ai or mobileYes, on your machineIn the terminal on your machine
Cloud sessionYes, for a plugin that reaches itNo
Per Anthropic's mods overview.

Guard rails work almost everywhere, headless included; the UI fun is terminal and Desktop only.

Security: read this before you install anything

Mod limits, per Anthropic's mods reference
Hook time per event
10 sTime inside next or API calls isn't counted, except $.clock.sleep.
.catch handler
1 sAll session.end hooks together get 1.5 s.
$.process.run
30 s → 10 min30 seconds by default, 10 minutes max.
$.model.complete
1,024 tokensDefault maxTokens; up to 64,000 or the model's limit.
Storage
4 MiBPer file for $.fs.read/write; total for $.store.
Redraws
10/s30/s in a terminal for a visible pane, an expanded band or the hint line.

Also from the reference: toasts last 4 seconds by default, and names are letters, digits, _ and -, up to 64 characters.

Team and enterprise controls

Org admins get a built-in guard, sec-default@builtin. It loads ahead of user mods when the machine has managed settings or a Team or Enterprise sign-in, protecting managed hooks, the system prompt, managed CLAUDE.md, settings reads and managed MCP tools. There, deny rules beat user mods, and managed PreToolUse hooks run first with a final block.

Which mods setting do you need? (per Anthropic's admin docs)
  1. 01Only org-approved mods (plus built-ins) should load?

    YES → allowManagedModsOnly, an option on the sec-default guardNO ↓

  2. 02Block --plugin-dir, --plugin-url and mods Claude writes?

    YES → disableSideloadFlagsNO ↓

  3. 03Org mods must run before or after user mods?

    YES → prependPlugins / appendPluginsNO ↓

  4. 04User mods should be able to override deny rules?

    YES → allowModsToOverrideDenyRules (otherwise deny rules win)NO ↓

  5. 05No hooks except managed ones, or none at all?

    YES → allowManagedHooksOnly (wider), disableAllHooks (widest)NO ↓

  6. Leave the defaults: the sec-default guard already loads ahead of user mods on managed or Team/Enterprise machines

For finer policy, write a policy mod: the plugin.register event shows each mod's e.uses.calls and e.tier, and returning { refuse: reason } keeps that mod from loading. Mods that review mods. Very meta.

How other coding agents do hooks

Hooks aren't new; Codex, Gemini CLI and Cursor all have them. What differs is where the code runs and whether it can touch the UI. Here's each tool as its own docs describe it today (we checked all four on October 2, 2026):

ToolHow hooks runWhere they're configuredExample eventsUI drawing
Claude Code modsJS/TS functions inside Claude Code's processA plugin (hooks/hooks.json)tool.call, tool.check, prompt.submit, ui.renderYes: pane, band, status line, toasts, built-in sites
OpenAI Codex CLICommand scripts or MCP tool calls, returning JSONhooks.json or config.toml in ~/.codex or <repo>/.codex; plugins can bundle hooksSessionStart, PreToolUse, PermissionRequest, PostToolUse, StopNone described
Google Gemini CLIScripts or programs over stdin/stdout JSON; on by default since v0.26.0settings.json; extensions can bundle hooksBeforeAgent, BeforeModel, BeforeTool, AfterTool, PreCompressNone described
CursorProcesses over stdio JSON, command- or prompt-based.cursor/hooks.json or ~/.cursor/hooks.jsonbeforeShellExecution, preToolUse, afterFileEdit, stopNone described (returns text messages only)
Per each vendor's hooks docs, checked Oct 2, 2026. Docs change fast; re-check before you pick a tool on this.

As those docs describe things today, the others run external commands or processes that answer yes, no or "here's some context". Claude Code's mods add in-process functions plus a UI surface: more power to build with, and more power for a bad mod to abuse.

Built-in mods and the official samples

Fun fact: you're already running mods. Open /plugin → Installed → Built-in and you'll see them. Even /diff is a mod now (Runtime Wire noticed too). Note that disableAllHooks, --bare and --safe-mode don't stop built-in mods.

The list: cc-plugin-agents-md (loads AGENTS.md as project instructions), cc-plugin-diff (/diff and its pane), cc-plugin-plugin-authoring (the plugin-authoring skill, no mod code), cc-plugin-sec-default (guards what your org manages), cc-plugin-telemetry (analytics records) and cc-plugin-you-should-know (a side agent that watches your back on longer tasks and leaves a note above the prompt; off by default).

Anthropic also put three sample mods in the claude-code-playground repo, shared as-is with no support: token-weather (a forecast of your context window above the prompt), blast-radius (holds a risky shell command like rm -rf, a force push, git reset --hard or a migration, shows what it would change, with Proceed and Cancel buttons) and replay-theater (adds /replay to step through the file edits Claude made last turn, one diff at a time).

Two starter mods to steal (as sketches)

Two mods that earn their spot on day one. These are our sketches against the API shapes in Anthropic's docs, not code from the docs, and we haven't run them.

Starter mods: vibe-coder vs team lead

🎧 Vibe-coder lane

illustrative sketch · untested

A context meter in the AbovePrompt band, so you see the window filling before the vibes run out.

Uses $.session.usage(), which the API docs say returns context tokens, window and percent, plus a Text element from $.ui.resolve(e).

javascript
// Illustrative sketch, not from Anthropic's docs. Untested.
// Check .claude-plugin/types/ for the exact render return shape.
export function register(on) {
  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
    // The band is shared by every mod — check the types; you may want to compose with await next(e)
    const usage = await $.session.usage()
    const { Text } = $.ui.resolve(e)
    return Text({ children: ['context ', usage.context.percent + '% of ' + usage.context.window] })
  })
}

🛡️ Team-lead lane

illustrative sketch · untested

A blast-radius-style hold: the docs' $.ui.ask pattern, with the regex widened to catch git push -f and migrations.

Proceed or Cancel, a log line when someone proceeds, and a safe default if nobody answers.

javascript
// Illustrative sketch, not from Anthropic's docs. Untested.
const RISKY = /\brm\s+-rf?\b|\bgit\s+reset\s+--hard\b|\bgit\s+push\b.*(--force|\s-f\b)|\bmigrate\b/

export function register(on) {
  on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
    if (!RISKY.test(e.command)) return next(e)
    let answer = 'Cancel'
    try {
      answer = await $.ui.ask('Blast radius check: ' + e.command, ['Proceed', 'Cancel'])
    } catch {}
    if (answer !== 'Proceed') return { deny: 'Held by the blast-radius check. Ask the user first.' }
    $.ui.log('proceeded: ' + e.command)
    return next(e)
  })
}

Both panels are ByteVancer's illustrative sketches, not code from Anthropic's docs, and untested. For the real thing, start from the official token-weather and blast-radius samples.

TL;DR of the TL;DR: mods turn Claude Code from an app you configure into an app you program. Huge for guard rails and dashboards, and a fresh supply-chain surface next to your secrets. Validate before installing, fail closed, keep real protection on your Git host. Then make the spinner say something unhinged. 🫡

FAQ

Questions, answered

Mods are plugins made of JavaScript or TypeScript event handlers that run inside Claude Code. When an event happens, such as a tool call, a submitted prompt or part of the interface being drawn, Claude Code calls the handler, which can observe it, rewrite it, or answer it so the original action never runs. Mods can also draw panes, a band above the prompt, status lines and toasts, and add commands. Anthropic announced them on October 1, 2026.

Want guard rails and dashboards built into your AI tooling?

We build AI-powered cloud applications and the developer tooling around them: agent workflows with real checks, safe defaults and the visibility your team needs. See our custom AI developer tooling for cloud apps.