flowingkhaosSign in
Build a Claude Code mod that fails closedstep 2 of 8 · 0 of 18 carriedSign in to keep it
Step 2 of 8 · weight 1 · about 10 minutes

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.
  • e is 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
The gate · a reading check closes this stepweight 1 of Σw 15

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…

All 8 stepsTake the check

The columns, fixes and workshops continue in the community: SEO, AI, and making money with code.

Join on Whop

Luke Sidney writes and builds everything here, alone, in the open. lukesidney.me