plugin-authoring
WHERE TO WRITE IT. Write each mod in its own child folder, /Users/asgeirtj/.claude/dev-mods/487721da-16ef-4b3b-90cf-6fb19db48601/<mod-name>/, three files written directly:
.claude-plugin/plugin.json:{ "name": "<mod-name>", "version": "0.1.0", "description": "<one line>" }hooks/hooks.json:{ "modules": ["./register.tsx"] }, one path, relative to that filehooks/register.tsx(or.ts): the hooks module,export const register: Register = (on, options) => { ... }, the type imported from'claude-code'
A mod that keeps values in $.state has a fourth file, types/index.d.ts: its type contract, declaring each value in interface PluginState under the mod's name, named in plugin.json as "types": "./types/index.d.ts". The module imports its value types from '../types', and claude plugin validate holds every $.state key the module names to that contract.
WHERE THE TYPES ARE. The engine writes them; there is no command to run. Before the mod has loaded: /private/tmp/claude-501/bundled-skills/2.1.299/3753dea880decbc121739bd42114cb7c/plugin-authoring/types/claude-code.d.ts, this build's API and built-in tools, written as this skill loaded (this process's own folder: after a restart the next load of this skill writes and names a new one). Once the engine has loaded the mod (from this mods folder, or a --plugin-dir folder it hot-reloads), <mod folder>/.claude-plugin/types/ holds the same for the editor: claude-code/index.d.ts, the API; claude-code-tools/index.d.ts, so e.tool === "Bash" narrows e; claude-code-mcp/index.d.ts, the MCP tools connected when the mod last reloaded; each dependencies plugin's contract; and a tsconfig.json the mod's own extends, so tsc -p <mod folder> type-checks it. The API file carries every event's input and result, every noun and method on $ with its doc comment and an example, and every element's props, in about 14,000 lines: grep it for the name at hand ('tool.call', Pane: {, export type ToolCallResult) and read the declaration it lands on.
WHAT HAPPENS WHEN THE TURN ENDS. Loading this skill through the Skill tool or its slash command starts the engine's watch on the folder that holds the mod folders. The first file written under it makes the engine ask the person, once, right then, while the turn goes on: "Enable hot reloading for this session?", with How does this work? first, then Enable for this session and Not now. That question is the switch, and the person alone answers it: no permission mode, rule or hook does. On Enable for this session the folder joins the session's plugin folders and the mod loads when the turn ends, whole, and each later edit reloads it when the turn that made the edit ends. The answer reaches you as a notice at the start of your next turn, one of: enabled, with what the load came to; declined (the mods are written and load the next time this session starts; the person can ask for the question again); still open (a new prompt from the person takes the question down, and the engine asks again when that turn ends); or off, with the reason (nobody could be asked, as under claude -p; the question got no pick three times; an organization's policy; an untrusted workspace). A process that restarted loads an enabled folder again by itself; otherwise its watch starts the next time this skill loads, and a manifest already in the folder raises the question when that turn ends.
A reload is a fresh load of the module: register runs again and session.start fires again. Values in $.state (the session's) and $.store (across sessions) are the host's and stay; the module's own variables start over.
THIS SESSION is drawn in a terminal. A pane the person opened seats at any width; one opened unasked seats from 144 columns, or 110 as $.ui.open's doc says, and waits below that. Where hot reloading is off as nobody could be asked, claude --plugin-dir <mod folder> starts a session with the mod loaded.
A mod in one paragraph
on(event, matcher?, hook) adds a hook, and every hook is ($, e, next): $ is the engine interface, each call spelled noun then method; e is the event's input, a plain frozen value; next(e) runs the plugins beneath and then the engine's own behaviour, resolving to the event's result. A hook that returns without next answers for itself; next({ ...e, x }) rewrites what the rest sees. The module runs in an environment of its own, with no DOM and no Node: $ reaches everything outside it. JSX compiles against the global h, and the elements come from the drawing surface's own table, const { Box, Text, Button } = $.ui.resolve(e), where e.surface is terminal, desktop, vscode or mobile.