Example: band above the prompt with the git branch, from $.process.run on a timer, held in $.state

A Claude Code skill that teaches an agent how to build Claude Mods: plugins whose behaviour is a TypeScript function-hook module that runs inside Claude Code. It ships five small example mods that the agent copies. Each example passes claude plugin validate, claude plugin test and tsc.
Tested with Claude Code 2.1.290. Mods need 2.1.287 or later and are on by default. The old CLAUDE_CODE_ENABLE_FUNCTION_HOOKS flag is ignored; remove it from your settings.
Claude Code also has a built-in plugin-authoring skill. That one knows your exact build. This skill sits on top of it: tested examples, traps seen on real builds, and how to prove and ship a mod.
At the Claude Code prompt:
/plugin install claude-mods-skill --marketplace BeLazy167/claude-mods-skill
Or from your shell:
claude plugin marketplace add BeLazy167/claude-mods-skill
claude plugin install claude-mods-skill@claude-mods-skill
Then ask Claude to build a mod. The creating-mods skill loads on its own.
skills/creating-mods/SKILL.md: the workflow, which example to copy, quick reference, loader rules, sharing.skills/creating-mods/references/gotchas.md: version-tagged traps from real builds.skills/creating-mods/references/examples.md: Anthropic and community mods worth reading.examples/:| Example | Shows |
|---|---|
hello-mod | Block a tool call; fail-closed .catch; smoke.sh proves the deny |
redact-output | Rewrite Bash output before the model reads it |
branch-band | Git branch above the prompt; $.process.run on a timer; $.state |
notes-pane | /note and /notes; a pane with a Button; $.store |
dice-tool | A tool the model can call |
Try one for a session: claude --plugin-dir examples/notes-pane.
The mods API changes between releases. After a Claude Code update, run:
scripts/check.sh
It validates, tests and type-checks every example against your build.
hooks/register.tsx 45 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Branch } from '../types'
5
6// Drawn values live in $.state, so they survive a hot reload and redraw readers.
7const branch = atom({ plugin: 'branch-band', key: 'branch' } as const, null)
8
9/** Reads the branch and the count of changed files. Null outside a git repo. */
10async function readBranch($: EngineInterface): Promise<Branch | null> {
11 const head = await $.process.run(['git', 'branch', '--show-current'])
12 if (head.exitCode !== 0) return null
13 const status = await $.process.run(['git', 'status', '--porcelain'])
14 const changed = status.stdout.split('\n').filter(Boolean).length
15 return { name: head.stdout.trim() || '(detached)', changed }
16}
17
18export const register: Register = (on) => {
19 // Work that outlives one dispatch starts in session.start and runs on $.clock.
20 on('session.start', async ($, e, next) => {
21 const refresh = async () => {
22 const now = await readBranch($)
23 await update($, branch, () => now)
24 }
25 await refresh()
26 $.clock.every(15_000, () => void refresh())
27 return next(e)
28 })
29
30 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
31 const now = await read($, branch)
32 // Nothing to show: let the engine draw its own band.
33 if (now === null) return next(e)
34
35 const { Box, Text } = $.ui.resolve(e)
36 return (
37 <Box>
38 <Text dimColor>git: </Text>
39 <Text>{now.name}</Text>
40 {now.changed > 0 && <Text color="yellow"> · {now.changed} changed</Text>}
41 </Box>
42 )
43 })
44}
45types/index.d.ts 8 lines1export type Branch = { name: string; changed: number }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'branch-band': { branch: Branch | null }
6 }
7}
8