Starter mod: a /mod-template command, a status line entry and a band above the prompt.

Mods for Claude Code (2.1.287+): plugins of function hooks that observe, rewrite or answer what Claude Code does and draw their own UI. Based on Getting started with Claude Code mods.
Landing page: https://cskwork.github.io/claude-code-mods/
| Mod | What it does |
|---|---|
token-weather | A one-line forecast of the context window above the prompt: ☀ Clear → ↯ Compact soon, tokens used, a sparkline of the last 12 turns, and how much the last turn added. |
blast-radius | Before a risky Bash command runs (recursive rm, git reset --hard, git clean -f, force push, git branch -D, git stash drop/clear, kubectl delete, SQL DROP/TRUNCATE, migrations), dry-runs what it would touch and asks Proceed or Cancel. Cancel or dismiss denies the call. |
template | Starter mod, not installed: a slash command, a status line entry, a band, and a $.state contract with a test. |
Inside Claude Code:
/plugin marketplace add cskwork/claude-code-mods
/plugin install token-weather@claude-code-mods
/plugin install blast-radius@claude-code-mods
/reload-plugins
scripts/new-mod.sh my-mod "One line about what it does"
claude --plugin-dir "$PWD/my-mod" # hot-reloads on every save
The script copies template/, renames it, adds it to .claude-plugin/marketplace.json, then runs claude plugin validate and claude plugin test on it.
A mod is three files plus an optional state contract:
my-mod/
├── .claude-plugin/plugin.json name, version, description, "types"
├── hooks/hooks.json { "modules": ["./register.tsx"] }
├── hooks/register.tsx export const register: Register = on => { ... }
├── types/index.d.ts PluginState contract for $.state values
└── tests/my-mod.test.ts claude plugin test my-mod
Rules that bite:
($, e, next). Call next(e) to pass on, next({ ...e, x }) to rewrite, return without it to answer.$.state (atom / read / update); module variables reset on every hot reload.$. UI elements come from $.ui.resolve(e).on('process.run', ($, e) => ({ value: ... }))..claude-plugin/types/ beside it, so tsc -p <mod> type-checks it (gitignored).for m in token-weather blast-radius template; do claude plugin validate $m && claude plugin test $m; donehooks/register.tsx 40 lines1// mod-template: the three moves a mod makes, in one file.
2// observe -> `await next(e)`, then act on the result (turn.complete)
3// answer -> return without `next` (command.run)
4// draw -> a ui.render hook returns a tree (AbovePrompt band)
5// Values live in `$.state` (atoms), never module variables: a hot reload resets those.
6import { atom, read, update } from 'claude-code'
7import type { Register } from 'claude-code'
8
9const NAME = 'mod-template'
10const turns = atom({ plugin: 'mod-template', key: 'turns' } as const, 0)
11
12export const register: Register = on => {
13 on('session.start', async ($, e, next) => {
14 await $.command.register({ name: NAME, description: 'Say how many turns this session has run' })
15 return next(e)
16 })
17
18 on('turn.complete', async ($, e, next) => {
19 const result = await next(e)
20 if (!e.agentId) {
21 const count = await update($, turns, n => n + 1)
22 $.ui.status(`${NAME}: ${count} turns`)
23 }
24 return result
25 })
26
27 on('command.run', { command: NAME }, async $ => ({ text: `${await read($, turns)} turns so far.` }))
28
29 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
30 const count = await read($, turns)
31 if (e.props.hasSurvey || count === 0) return next(e)
32 const { Box, Text } = $.ui.resolve(e)
33 return (
34 <Box paddingX={1}>
35 <Text dimColor>{`${NAME}: ${count} turns`}</Text>
36 </Box>
37 )
38 })
39}
40types/index.d.ts 9 lines1// The mod's state contract: every `$.state` value the module names, under the mod's name.
2export type ModTemplateTurns = number
3
4declare module 'claude-code' {
5 interface PluginState {
6 'mod-template': { turns: ModTemplateTurns }
7 }
8}
9