How to Create a Claude Code Mod
You can describe a mod and let Claude write it, or write the three files yourself. Both paths end with the same loop: load, try, validate, test.
Last checked against the official docs: 2026-10-02
Option A: ask Claude to write it
Claude Code has a built-in skill called plugin-authoring that knows which events and methods your version supports. In an interactive session, describe what you want:
make a mod that shows the current git branch above the prompt- Claude writes the mod into
~/.claude/dev-mods/<session-id>/. Because~/.claudeis a protected path, you approve each file. - On the first file, Claude Code asks whether to enable hot reloading. Choose Enable for this session so the mod loads when the turn ends and reloads after every change.
- Check it in
/plugin→ Installed, then try it. If it isn't right, tell Claude what to change.
~/mods/git-branch) and load it with claude --plugin-dir ~/mods/git-branch. It won't load in claude -p, in untrusted folders, or when mods are disabled.Option B: write it yourself
A small mod is three files:
my-mod/
├── .claude-plugin/
│ └── plugin.json # the plugin manifest
└── hooks/
├── hooks.json # points Claude Code at your code file
└── register.js # your code: the "hooks module"The hooks module exports a register(on) function. Each on(event, handler) call subscribes a handler, and every handler receives ($, e, next): the mods API, the event, and a function that passes the event on. This tiny mod counts tool calls and adds the count to the spinner:
let calls = 0
export function register(on) {
// Before every tool call: count it, then let it run unchanged
on('tool.call', async ($, e, next) => {
calls += 1
$.ui.invalidate('ui.render')
return next(e)
})
// When the spinner is drawn: keep it, add a suffix
on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
return next({ ...e, props: { ...e.props, suffix: ' · tools: ' + calls } })
})
}Load it with claude --plugin-dir ./my-mod. Claude Code hot-reloads it whenever you save.
Three things a handler can do
- Observe: note the event and call
next(e)unchanged. - Rewrite: call
next()with a changed event. - Answer: return a result without calling
next, so the default behavior never runs (for example, refusing a command).
Anything outside your own code (drawing, adding a command, calling a model, reading a file, running a process, making a request) goes through $: $.ui, $.command, $.model, $.fs, $.process, $.http, $.store and more. The hooks module has no Node.js APIs of its own.
Get the exact types for your version
Each time Claude Code loads a mod from --plugin-dir, it writes TypeScript declarations into .claude-plugin/types/ inside the mod folder. claude-code/index.d.ts lists every event and method in the version you are running. Events can change between releases, so trust that file over any web page, including this one.
Validate before you run
claude plugin validate ./my-modThe output lists the events your module hooks and every $ method it calls. If an event you expected is missing, Claude Code won't call that hook either. Common causes:
- A misspelled event name, such as
tool.calls. - An event name passed as a variable instead of a string literal.
- A
$call that isn't written out in full, such as$.store.get(...). - Using
requireor a dynamicimport(). Write ES modules with top-level imports.
Test it
Put tests in tests/ and run claude plugin test. Tests fire events at your hooks and check what they did, with no session, sign-in or network needed.
Share it
Version it in plugin.json and publish it through a plugin marketplace. Avoid names that look like Anthropic's own, such as names starting with claude-; validate rejects them. Say in your README which Claude Code version you tested with.
Next: browse existing mods for patterns to copy, or fix a mod that does nothing.
Sources: Create a mod, Mods overview, Mods API.