Read it, then pass the check at the end to open step 3.
Tell a mod from a skill, a hook and an MCP server
Updated
What this step is for
Before you write a hook, know which of four tools you are holding. Most of what people want from a mod is cheaper as one of the other three.
| Tool | What it is | Who acts | Use it when |
|---|---|---|---|
| Skill | a Markdown file of instructions | the model reads it, and may not follow it | you want the model to work a certain way |
| Settings hook | a shell command Claude Code runs on an event | the harness, through JSON in and out | one check, in any language, on a tool call or a stop |
| MCP server | tools the model can call | the model decides to call them | the model needs a new ability |
| Mod | TypeScript functions inside Claude Code | the engine, on every event | you need to draw something, ask a question, or hold a call with state |
A mod is three files:
hello-mod/
├── .claude-plugin/plugin.json the name and a one-line description
└── hooks/
├── hooks.json which module to load
└── register.ts the hooks
Every hook has the same shape, ($, e, next):
$is the engine:$.ui,$.fs,$.process,$.session, … each call is noun then method.eis the event’s input, frozen.next(e)runs everything beneath your hook and the engine’s own behaviour, and gives you the result.
So a hook makes one of three moves. It observes (const r = await next(e), look at r,
return it), rewrites (next({ ...e, command: safer })), or answers for itself
(return { deny: 'why' } without calling next). Every mod in this workshop is one of these.
Do it
mkdir -p ~/claude-mods/hello-mod/.claude-plugin ~/claude-mods/hello-mod/hooks
cd ~/claude-mods/hello-mod
.claude-plugin/plugin.json:
{ "name": "hello-mod", "version": "0.1.0", "description": "Says hello when a session starts.", "author": { "name": "you" } }
hooks/hooks.json:
{ "modules": ["./register.ts"] }
hooks/register.ts:
import type { Register } from 'claude-code'
export const register: Register = on => {
on('session.start', async ($, e, next) => {
$.ui.toast('hello-mod is loaded')
return next(e)
})
}
The 'claude-code' import is types only; at run time it is empty. The engine writes the full
API, about 20,000 lines with an example on every call, into .claude-plugin/types/ the first
time it loads your mod. In Claude Code, /plugin-authoring lays the same file before that and
tells you where.
Check it
claude plugin validate ~/claude-mods/hello-mod
It ends with Validation passed and lists what the module hooks (session.start) and calls
($.ui.toast). Then start a session with the mod loaded and the toast appears:
claude --plugin-dir ~/claude-mods/hello-mod
Check what you read in “Tell a mod from a skill, a hook and an MCP server”
Three questions drawn from a larger pool, so no two papers are quite the same. Two right passes it and opens the next step. The questions are public; scoring them needs an account, and the key never leaves the server.
Loading the check…