Make your first Claude Code mod
A step-by-step tutorial for first-timers. In about ten minutes you build a small mod that counts tool calls in the status line and shows a toast when Claude is done.
Before you start
Mods are built on function hooks, a part of Claude Code that is in early access. The API can change between releases, so use a recent version of Claude Code and expect to adjust your mod now and then. You need no build step and no dependencies: a mod is a folder with three small files.
What you will build
A mod called tool-counter. While Claude works, the status line shows how many tool calls the current turn has made. When the turn ends, a toast tells you the total. It only reads events and draws on screen, so it is a safe first project.
Step 1: create the folder
Make a folder named tool-counter anywhere on your machine, with this layout:
tool-counter/
.claude-plugin/
plugin.json
hooks/
hooks.json
register.tsStep 2: describe the mod
.claude-plugin/plugin.json is the manifest. It gives the mod a name, a version and a one-line description.
{
"name": "tool-counter",
"version": "0.1.0",
"description": "Counts tool calls per turn in the status line and shows a toast when the turn is done."
}hooks/hooks.json tells Claude Code which file holds your hooks. The path is relative to this file.
{ "modules": ["./register.ts"] }Step 3: write the hooks
hooks/register.ts is the mod itself. It exports one function, register, which receives on. You call on once for every event you want to hook into.
import type { Register } from 'claude-code'
export const register: Register = on => {
let tools = 0
on('prompt.submit', ($, e, next) => {
tools = 0
$.ui.status(undefined)
return next(e)
})
on('tool.call', ($, e, next) => {
tools += 1
$.ui.status(`${tools} tool calls this turn`)
return next(e)
})
on('turn.complete', ($, e, next) => {
$.ui.toast(`Done after ${tools} tool calls`)
return next(e)
})
}How it works
Every hook receives the same three arguments:
$is your handle on Claude Code.$.ui.status(text)sets a status line entry and$.ui.toast(text)shows a toast. Passingundefinedtostatusclears the entry.eis the event: the prompt that was sent, the tool that is being called, and so on.next(e)hands the event on to other mods and to Claude Code itself.
The rule to remember: return next(e) unless you mean to change what happens. A hook that returns without calling next answers the event by itself. That is how a mod blocks a tool call, and it is also the most common beginner mistake.
Step 4: check and run it
Let Claude Code check the mod before you load it. The command reads the manifest and your hooks the way the engine will, and reports anything it would refuse.
claude plugin validate ./tool-counterThen start a session with the mod loaded from disk:
claude --plugin-dir ./tool-counterAsk Claude to do something that needs a few tools, such as listing the files in a folder and reading one of them. The counter appears in the status line and the toast shows when the turn ends. While the session is open, saving register.ts reloads the mod, so you can change it and try again without restarting.
If nothing happens
- Run
claude plugin validateagain and read every line of its output. - Start Claude Code with
claude --debug. The debug log names each hook the engine skipped and why. - Check that every hook returns
next(e). - Update Claude Code. Older versions do not load function hooks.
Where to go next
- Change a prompt. In a
prompt.submithook, pass a changed event on withnext({ ...e, text }). - Guard a tool. Match one tool with
on('tool.call', { tool: 'Bash' }, hook)and return{ deny: 'reason' }to block a call. - Add a command. Register a slash command and answer it from a
command.runhook. - Draw a pane. Open a pane and render it from a
ui.renderhook for a live view next to the conversation.
When your mod does something useful, put it in a public GitHub repository. Leave your email on the homepage and we will let you know when submissions to the directory open.
Browse the directory
Reviewed mods, pinned commits, official install links.