Original Pages

Free guide · Claude Code

Claude Code mods: live panes, guards and slash commands

Level · Advanced

A mod is a plugin made of function hooks that hot-reloads inside a running Claude Code session — in the terminal or in the desktop Code tab. It can draw a pane, add a status line entry, block a tool call, or answer a slash command of its own. This page shows what a mod is made of and three small examples.

The short version

  1. A mod is three files in a folder — a manifest, a hooks list, and one module that registers hooks — anatomy of a mod.
  2. Every hook has the shape on(event, matcher?, ($, e, next) => ...) — how hooks work.
  3. Easiest start: ask Claude Code to build one for you. Then validate it with claude plugin validate — building and testing.

Never met a plugin before? Skip the box — the explanation starts below.

New to Claude Code? Start here

What a mod is

Claude Code plugins normally bundle things like commands and hooks that load when a session starts. A mod is a plugin written as function hooks that hot-reload: you edit the code and the running session picks it up, without restarting.

Mods run inside the session itself, so they can change what you see (a pane, a band above the prompt, a status line entry, a toast) and what happens (a tool call, a prompt, the system prompt, a slash command).

The detail

Anatomy of a mod

A mod is a folder with three files:

my-mod/
├── .claude-plugin/
│   └── plugin.json
└── hooks/
    ├── hooks.json
    └── register.tsx

.claude-plugin/plugin.json is the manifest:

{
  "name": "my-mod",
  "version": "0.1.0",
  "description": "One line about what this mod does"
}

hooks/hooks.json lists the modules to load:

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

hooks/register.tsx exports a register function. The Register type is imported from claude-code:

import type { Register } from 'claude-code'

export const register: Register = (on, options) => {
  // on(event, matcher?, ($, e, next) => ...)
}

How hooks work

Inside register you call on(event, matcher?, ($, e, next) => ...) once per hook. The matcher is optional and narrows the hook, for example to one tool. The callback receives three things:

  • $ — the engine interface: UI, commands, audio, agents and more.
  • e — the event input.
  • next(e) — runs the rest of the hook chain.

Returning without calling next means your hook answers for itself. Calling next({ ...e, x }) rewrites what the rest of the chain sees.

Visible elements are written in JSX, using components from $.ui.resolve(e): Box, Text and Button.

What a mod can do

GoalHow
Pane / live view$.ui.open({ id, title }), drawn by a ui.render hook on { component: 'Pane' }
Band above the promptui.render hook on { component: 'AbovePrompt' }
Status line entry$.ui.status(text); undefined clears it
Toast$.ui.toast(text)
Block or rewrite a tool callon('tool.call', { tool: 'Bash' }, ...) returning { deny: reason } or next({ ...e, ... })
Change a prompton('prompt.submit', ...) with next({ ...e, text })
Add to the system prompton('prompt.compose', ...)
Slash command$.command.register({ name, description }) in session.start, answered by a command.run hook returning { text }
Play a sound$.audio.play({ asset })
Agents and sessions$.agent.list() lists this session’s subagents and teammates, $.agent.spawn(...) starts one, $.session.send({ to, text }) messages another session

Also available on $: $.clock (timers), $.fs, $.process, $.model and $.http.fetch. One limit: a mod cannot list other sessions on the machine.

Example: a status line entry

The smallest useful mod puts one line of text in the status line when the session starts:

import type { Register } from 'claude-code'

export const register: Register = (on, options) => {
  on('session.start', ($, e, next) => {
    $.ui.status('my-mod is loaded') // pass undefined later to clear it
    return next(e)
  })
}

Example: a Bash guard

A tool.call hook matched on Bash can refuse a call by returning { deny: reason }, or let it through with next(e):

import type { Register } from 'claude-code'

export const register: Register = (on, options) => {
  on('tool.call', { tool: 'Bash' }, ($, e, next) => {
    // Schematic: the exact field that holds the Bash command on `e`
    // is not covered here. Inspect `e` in your own mod to find it.
    if (JSON.stringify(e).includes('rm -rf')) {
      return { deny: 'my-mod: rm -rf is blocked in this session' }
    }
    return next(e)
  })
}

This example is deliberately schematic. The deny and next shapes are the documented part; how you read the command out of e is for you to check.

Example: a /hello command

A slash command takes two hooks: register it in session.start, then answer it from a command.run hook that returns { text }:

import type { Register } from 'claude-code'

export const register: Register = (on, options) => {
  on('session.start', ($, e, next) => {
    $.command.register({ name: 'hello', description: 'Say hello' })
    return next(e)
  })

  // Schematic: check `e` for which command ran before answering.
  on('command.run', ($, e, next) => {
    return { text: 'Hello from my-mod' }
  })
}

Also schematic: a real mod should check which command ran before answering.

Building, testing and sharing

The easiest way to start is to ask Claude Code to build a mod; it loads the plugin-authoring skill. You are asked once “Enable hot reloading for this session?”

Then check the folder, test it, and run it:

claude plugin validate ./my-mod
claude plugin test ./my-mod
claude --plugin-dir ./my-mod

To share a mod, publish it in a repo as a marketplace and install it with:

/plugin install my-mod --marketplace <owner>/<repo>

Based on the plugin-authoring docs in Claude Code build 2.1.294. Mod hooks are new, so check the in-product skill for the current event list and signatures.

What to read next

New to the CLI? Start with Getting started with Claude Code. For shaping behaviour with a plain file instead of code, read CLAUDE.md that actually works.