SLOPSHOPPER

notes-pane

Example: /note saves to $.store, /notes opens a pane with a Clear button

newpanecommand
★ 6v0.1.0MITupdated 2026-10-06BeLazy167/claude-mods-skill/examples/notes-pane
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · notes-pane
│ ┃ Notes ✕ › fix the failing auth test and add an audit log call │ ┃ No notes. Add one with /note. │ ┃ [ Clear ] ⏺ Read(src/auth.ts) │ ⎿ Read 6 lines │ ⏺ Update(src/auth.ts) │ ⎿ Added 2 lines, removed 1 line │ ⏺ Bash(bun test) │ ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /note │ ⎿ notes-pane: Usage: /note <text> │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Notes
No notes. Add one with /note. [ Clear ]
README

claude-mods-skill

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.

Install

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.

What is inside

  • 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/:
ExampleShows
hello-modBlock a tool call; fail-closed .catch; smoke.sh proves the deny
redact-outputRewrite Bash output before the model reads it
branch-bandGit branch above the prompt; $.process.run on a timer; $.state
notes-pane/note and /notes; a pane with a Button; $.store
dice-toolA tool the model can call

Try one for a session: claude --plugin-dir examples/notes-pane.

Keeping it current

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.

Links

Source 2 files
hooks/register.tsx 56 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Note } from '../types'
5
6const PANE = 'notes'
7// $.state holds what the pane draws this session. $.store keeps it across sessions.
8const notes = atom({ plugin: 'notes-pane', key: 'notes' } as const, [] as Note[])
9
10export const register: Register = (on) => {
11  on('session.start', async ($, e, next) => {
12    const saved = await $.store.get('notes')
13    await update($, notes, () => (Array.isArray(saved) ? saved.map(String) : []))
14    await $.command.register({ name: 'note', description: 'Save a note', argumentHint: '<text>' })
15    await $.command.register({ name: 'notes', description: 'Show saved notes in a pane' })
16    return next(e)
17  })
18
19  // A command hook answers alone. Its text is the transcript row the model also reads.
20  on('command.run', { command: 'note' }, async ($, e) => {
21    const text = e.args.trim()
22    if (text === '') return { text: 'Usage: /note <text>' }
23    const all = await update($, notes, (list) => [...list, text])
24    await $.store.set('notes', all)
25    return { text: `Saved note ${all.length}.` }
26  })
27
28  on('command.run', { command: 'notes' }, async ($) => {
29    await $.ui.open({ id: PANE, title: 'Notes' })
30    return { text: 'Notes pane opened.' }
31  })
32
33  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
34    const { Box, Button, Text } = $.ui.resolve(e)
35    const list = await read($, notes)
36    return (
37      <Box flexDirection="column">
38        {list.length === 0 && <Text dimColor>No notes. Add one with /note.</Text>}
39        {list.map((note, i) => (
40          <Text>
41            {i + 1}. {note}
42          </Text>
43        ))}
44        <Button
45          key="clear"
46          label="Clear"
47          onPress={async () => {
48            await update($, notes, () => [])
49            await $.store.delete('notes')
50          }}
51        />
52      </Box>
53    )
54  })
55}
56
types/index.d.ts 8 lines
1export type Note = string
2
3declare module 'claude-code' {
4  interface PluginState {
5    'notes-pane': { notes: Note[] }
6  }
7}
8