Free guide · Claude Code
Claude Code mods: live panes, guards and slash commands
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
- A mod is three files in a folder — a manifest, a hooks list, and one module that registers hooks — anatomy of a mod.
- Every hook has the shape
on(event, matcher?, ($, e, next) => ...)— how hooks work. - 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.
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).
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
| Goal | How |
|---|---|
| Pane / live view | $.ui.open({ id, title }), drawn by a ui.render hook on { component: 'Pane' } |
| Band above the prompt | ui.render hook on { component: 'AbovePrompt' } |
| Status line entry | $.ui.status(text); undefined clears it |
| Toast | $.ui.toast(text) |
| Block or rewrite a tool call | on('tool.call', { tool: 'Bash' }, ...) returning { deny: reason } or next({ ...e, ... }) |
| Change a prompt | on('prompt.submit', ...) with next({ ...e, text }) |
| Add to the system prompt | on('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-modTo 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.