SLOPSHOPPER

kitchen-sink

Interactive lab: panes, controls, notes, events, prompt recipes and a demo tool.

panebandspinnerguardcommandtoast
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · kitchen-sink
│ ┃ Kitchen Sink ✕ › fix the failing auth test and add an audit log call │ ┃ CLAUDE MOD KITCHEN SINK │ ┃ ⏺ Read(src/auth.ts) │ ┃ Renderer: terminal · experimental learning ⎿ Read 6 lines │ ┃ lab ⏺ Update(src/auth.ts) │ ┃ ⎿ Added 2 lines, removed 1 line │ ┃ [ [recipes] ] [ events ] [ notes ] ⏺ Bash(bun test) │ ┃ ⎿ 3 pass, 1 fail │ ┃ Explore a real mod, one small interaction at │ ┃ a time. ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ │ ┃ Start with an empty input. Draft buttons ✻ Worked for 42s · done 4:20 PM │ ┃ fill it; review, then send. │ ┃ › /kitchen │ ┃ [ Click counter: 0 ] ⎿ kitchen-sink: Kitchen Sink opened. Try the tabs, then /kitchen-s │ ┃ │ ┃ [ Draft a brief prompt ] │ ┃ │ ┃ [ Draft a demo tool request ] │ ┃ │ ┃ The tool has a 1–3 portion guard. The note │ ┃ tab persists only what you explicitly save. │ ✻ Thinking · kitchen: 9 calls… ⟨Claude Code's own drawing⟩ Kitchen Sink · 9 tools · 1 turns [ Open lab ] ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
⟨Claude Code's own drawing⟩ Kitchen Sink · 9 tools · 1 turns [ Open lab ]
Pane · Kitchen Sink
CLAUDE MOD KITCHEN SINK Renderer: terminal · experimental learning lab [ [recipes] ] [ events ] [ notes ] Explore a real mod, one small interaction at a time. Start with an empty input. Draft buttons fill it; review, then send. [ Click counter: 0 ] [ Draft a brief prompt ] [ Draft a demo tool request ] The tool has a 1–3 portion guard. The note tab persists only what you explicitly save.
README

Claude Mod Kitchen Sink

Learn Claude customization by changing one working example at a time.

A practical, MIT-licensed workshop for people who want to understand plugins and build their first Claude Code mod. Includes a Cowork workflow plugin, an interactive UI mod, and an eight-lesson course with four illustrated explainers, exercises, checkpoints, and progress saved in your browser.

Start the course → · Try the experiments · Read the source · Official references

Independent community project. Not affiliated with or endorsed by Anthropic.

Conceptual illustration: an event enters your hook; observe forwards it, rewrite forwards a changed copy, and answer returns a result locally.

Follow the event through a hook, then try all three choices in the browser explainer.

Follow the course

The course homepage lets you choose a focused path:

SectionLessons
FoundationsBuilding blocks, choose a surface
Hands-onCowork workflow, events and hooks, UI and state, plugin anatomy
Finish and shareYour first change, check and share

Every lesson has goals, direct links to the exercise and optional checkpoint, and navigation that follows your chosen path. Complete & continue saves the current lesson and opens the next one. The homepage names the lesson you can resume. Switch paths without losing completion marks, or use Progress settings → Reset completion marks → Undo reset to recover a reset before leaving the page. Expand each visual explainer when you want the diagram alongside the text.

Only known lesson IDs, completion marks, and your selected path are saved in local browser storage. No account is needed. A ?path=cowork or ?path=code link carries the learning path for someone you share it with; it does not share your progress. If storage is blocked, navigation still works and the page explains that completion will not persist. The original guide remains a quick reference. Read the course UX notes for the design decisions and a focused browser-check checklist.

Choose your starting point

You want to…Start hereWhat you will see
Understand the idea before installingEvent-and-hook lessonA labeled browser simulation of observing, rewriting and answering an event
Customize a workflow in CoworkCowork pluginReusable commands that turn supplied notes into an experiment card
Add buttons, tabs and a paneClaude Code modAn interactive lab inside a supported Claude Code surface
Build your own versionFirst-change tutorialAn explained change with an observable result

Cowork and the desktop Code tab are different surfaces. The Cowork example adds workflow commands. The custom UI example targets Claude Code, including the desktop Code tab. This project does not claim that Cowork draws the mod’s pane. See the dated compatibility notes.

Try the Cowork workflow

You need: Claude with plugin upload available on your plan/account. No terminal, Node, Python or connected service is needed if you download the prebuilt ZIP.

  1. Download kitchen-cowork.zip and keep it zipped.
  2. In the Cowork / unified Claude desktop experience, open Customize → Plugins and use the custom upload option. UI labels and organization policy can vary.
  3. Run /kitchen-cowork:tour.
  4. Attach the synthetic workshop notes, then run /kitchen-cowork:experiment.

Expect: an experiment card with Goal, Smallest change, Observable result, and Next question. This is a model following a reusable prompt, so outputs can vary. It uses normal conversation-model usage.

Change it: edit the fields in experiment.md, rebuild with npm run package:cowork from the repository root, re-upload dist/kitchen-cowork.zip, and compare. Building the ZIP requires Python 3.9+; the npm wrapper also requires Node.

Try the UI mod

You need: Claude Code in a supported interactive surface. The documented terminal minimum is 2.1.287; desktop Code minimum is 2.1.286. Run claude --version in a terminal or /status inside the Code tab: the versions can differ. Newer API behavior can vary; use the generated declarations for your build.

Terminal: the shortest development loop

Clone the repository, enter its root, and launch the mod:

git clone https://github.com/reachjalil/claude-mod-kitchen-sink.git
cd claude-mod-kitchen-sink
claude --plugin-dir ./plugins/kitchen-sink

At the Claude prompt, enter:

/kitchen

Expect: a pane with recipes, events, and notes tabs. Click the counter; the number changes without a model request. Use a draft button with an empty prompt box, review the text, then send it yourself. Run /kitchen-status to inspect counters and reported render surfaces.

No npm install or build step is needed to load the mod. Edit register.js while the --plugin-dir session is open; Claude watches that directory and reloads the module. Counters reset on reload; saved notes remain.

Desktop Code tab: install the local marketplace

First clone or download this repository. In a local Code session in Claude Desktop, run the following. Replace the example absolute path with your clone’s path:

/plugin marketplace add /absolute/path/to/claude-mod-kitchen-sink
/plugin install kitchen-sink@claude-mod-kitchen-sink
/reload-plugins
/kitchen

Use the direct terminal directory for active editing; an installed marketplace copy can differ from your working tree. The mod passes native desktop element-contract tests. Final layout and installation in the desktop app still need the manual acceptance pass on your build.

What does each recipe teach?

RecipeTry itWhy it matters
Pane, tabs and button/kitchen, then click the counterInteractive UI can update without asking the model
Draft a promptPress a draft button with empty inputFill a draft while the user controls sending; existing text is preserved
Rewrite one promptSend [ks:brief] Explain panesAn explicit prefix opts in to a changed event
Register a toolAsk to mix 2 imaginary portionsA mod can provide a tool without a separate server process
Guard a toolRead the 1–3 portion validationAnswer or deny only the plugin’s own imaginary operation
Observe eventsUse a tool and open EventsCount calls without storing tool contents
Compose UIInspect the band above the promptKeep downstream drawings from other mods
Restyle a detailWatch the spinner during activityExtend the normal rendering instead of replacing the whole view
Save a noteSave, reopen, deleteLearn local persistence and its cross-session limits
Provide a fallback/kitchen-statusA text command is useful when no UI renderer is available

Read the annotated walkthrough for $, e, next, the event chain, and state lifetimes.

Check a change

For the complete local check set: Node.js 20+, Python 3.9+, and a recent Claude Code. No dependency installation is required.

npm test                # Behavioral regression tests; includes packaging checks
npm run validate       # Real Claude static validator, both plugins + marketplace
npm run test:native    # Real mod-host tests, terminal + desktop element contracts
npm run site:build     # Builds the guide, current source excerpts, and download ZIP
npm run site:check     # Checks guide links, snippet freshness, and download members

Native validation and host tests were run with Claude Code 2.1.301. Browser testing of the guide is separate from desktop-mod testing. See the validation record for exact scope and remaining gaps.

Project map

plugins/kitchen-sink/     Real mod: manifest, hooks module, native host test
plugins/kitchen-cowork/   Workflow commands and synthetic sample notes
site/                    Course content, reference HTML, CSS, browser-only JS
scripts/                 Course rendering, validation, packaging, site build/check
tests/                  Behavioral and distribution tests
docs/                   Explainers, tutorials, troubleshooting, references

The mod uses no network, process, automatic model, or session-messaging APIs. Only an explicitly saved scratch note persists in the plugin’s local store. Mods in general run with your machine access; inspect code before loading. Security and data.

Get unstuck, extend it, share it

Both plugin bundles and the guide use the MIT license. The self-hosted Manrope font uses the SIL Open Font License; see asset notices. The event-flow brief and course illustration briefs include reusable prompts and text explanations. Fork it, teach from it, or use a recipe as a starting point. Keep demonstrations synthetic and record the surface/version you actually checked.

Source 1 files
hooks/register.js 150 lines
1// No imports or build step. Keep $ API calls explicit for Claude's static validator.
2// UI callbacks run on a click; no background model requests or session messages.
3export function register(on) {
4  let tab = 'recipes';
5  let note = '';
6  let clicks = 0;
7  let tools = 0;
8  let turns = 0;
9  let events = [];
10  const record = (label) => { events = [...events, label].slice(-12); };
11
12  on('session.start', async ($, e, next) => {
13    // A damaged/unavailable note store should not prevent the lab from loading.
14    try {
15      const saved = await $.store.get('scratch-note');
16      note = typeof saved === 'string' ? saved.slice(0, 500) : '';
17    } catch {
18      record('note store unavailable; starting with an empty note');
19    }
20    record('session.start');
21    await $.tool.register({
22      name: 'mix',
23      description: 'Kitchen Sink demo only. Mix 1–3 imaginary portions; no real-world action.',
24      inputSchema: { type: 'object', properties: { portions: { type: 'integer', minimum: 1, maximum: 3 } }, required: ['portions'], additionalProperties: false },
25    });
26    await $.command.register({ name: 'kitchen', description: 'Open the Kitchen Sink learning pane', immediate: true });
27    await $.command.register({ name: 'kitchen-status', description: 'Print lab counters and available render surfaces', immediate: true });
28    return next(e);
29  });
30
31  on('command.run', { command: 'kitchen' }, async ($) => {
32    const placed = await $.ui.open({ id: 'kitchen-sink-lab', title: 'Kitchen Sink', focus: true, closeOnEscape: true });
33    return { text: placed.isPlaced ? 'Kitchen Sink opened. Try the tabs, then /kitchen-status.' : 'No pane was placed. Use /kitchen-status; try a local Desktop Code tab or interactive terminal.' };
34  });
35
36  on('command.run', { command: 'kitchen-status' }, async ($) => {
37    const surfaces = await $.session.surfaces();
38    return { text: `Kitchen Sink | tools: ${tools} | turns: ${turns} | clicks: ${clicks} | surfaces: ${surfaces.join(', ') || 'none (headless or no renderer)'}` };
39  });
40
41  // Observe a call without keeping arguments, prompts, results or file paths.
42  on('tool.call', async ($, e, next) => {
43    tools += 1;
44    record('tool.call');
45    $.ui.invalidate('ui.render');
46    return next(e);
47  });
48
49  // A deliberately narrow gate: it only handles this plugin's imaginary tool.
50  on('tool.call', { tool: 'mcp__kitchen-sink__mix' }, async ($, e) => {
51    if (!Number.isInteger(e.portions) || e.portions < 1 || e.portions > 3) {
52      record('mix: refused invalid portion count');
53      return { deny: 'Demo limit: choose an integer from 1 to 3 portions.' };
54    }
55    record('mix: completed');
56    return { result: `Mixed ${e.portions} imaginary portion(s). Nothing was written, purchased or sent.` };
57  }).catch(async () => ({ deny: 'Kitchen demo tool failed; no action was taken.' }));
58
59  // Opt in on one prompt only. Other prompts pass through byte for byte.
60  on('prompt.submit', async ($, e, next) => {
61    if (!e.text.startsWith('[ks:brief] ')) return next(e);
62    record('prompt.submit: brief recipe');
63    return next({ ...e, text: e.text.slice('[ks:brief] '.length) + '\n\nPlease answer in at most three concise bullet points.' });
64  });
65
66  on('turn.complete', async ($, e, next) => {
67    turns += 1;
68    record('turn.complete');
69    $.ui.invalidate('ui.render');
70    return next(e);
71  });
72
73  on('ui.render', { component: 'Spinner' }, async ($, e, next) =>
74    next({ ...e, props: { ...e.props, suffix: `${e.props.suffix || ''} · kitchen: ${tools} calls` } })
75  );
76
77  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
78    const { Box, Text, Button } = $.ui.resolve(e);
79    const others = await next(e);
80    return Box({ flexDirection: 'column', children: [
81      ...(others ? [others] : []),
82      Box({ flexDirection: 'row', columnGap: 2, children: [
83        Text({ dimColor: true, children: [`Kitchen Sink · ${tools} tools · ${turns} turns`] }),
84        Button({ key: 'open-lab', label: 'Open lab', onPress: async () => {
85          await $.ui.open({ id: 'kitchen-sink-lab', title: 'Kitchen Sink', focus: true, closeOnEscape: true });
86        } }),
87      ] }),
88    ] });
89  });
90
91  on('ui.render', { component: 'Pane' }, async ($, e, next) => {
92    if (e.requestId !== 'kitchen-sink-lab') return next(e);
93    const { Box, Text, Button, Input } = $.ui.resolve(e);
94    const tabs = ['recipes', 'events', 'notes'].map((name) => Button({
95      key: `tab-${name}`, label: name === tab ? `[${name}]` : name,
96      onPress: () => { tab = name; $.ui.invalidate('ui.render'); },
97    }));
98    let body = [];
99    if (tab === 'recipes') body = [
100      Text({ children: ['Explore a real mod, one small interaction at a time.'] }),
101      Text({ dimColor: true, children: ['Start with an empty input. Draft buttons fill it; review, then send.'] }),
102      Button({ key: 'counter', label: `Click counter: ${clicks}`, onPress: () => { clicks += 1; $.ui.invalidate('ui.render'); } }),
103      Button({ key: 'draft-brief', label: 'Draft a brief prompt', onPress: async () => {
104        const draft = await $.prompt.read();
105        if (draft.text) {
106          await $.ui.toast('Your draft is still there. Clear or send it before loading this example.');
107          return;
108        }
109        await $.prompt.fill({ text: '[ks:brief] Explain how Claude Code mods work.', mode: 'append' });
110      } }),
111      Button({ key: 'draft-mix', label: 'Draft a demo tool request', onPress: async () => {
112        const draft = await $.prompt.read();
113        if (draft.text) {
114          await $.ui.toast('Your draft is still there. Clear or send it before loading this example.');
115          return;
116        }
117        await $.prompt.fill({ text: 'Use the kitchen-sink mix tool to mix 2 imaginary portions.', mode: 'append' });
118      } }),
119      Text({ children: ['The tool has a 1–3 portion guard. The note tab persists only what you explicitly save.'] }),
120    ];
121    if (tab === 'events') body = [
122      Text({ bold: true, children: [`${tools} tool calls · ${turns} turns · ${clicks} clicks`] }),
123      Text({ dimColor: true, children: ['Last 12 labels, in memory only. No prompt or tool contents are recorded.'] }),
124      ...events.map((label) => Text({ children: [label] })),
125      Button({ key: 'reset-events', label: 'Reset counters and event labels', onPress: () => {
126        tools = 0; turns = 0; clicks = 0; events = []; $.ui.invalidate('ui.render');
127      } }),
128    ];
129    if (tab === 'notes') body = [
130      Text({ children: ['One local scratch note, shared by this plugin across sessions. Last save wins.'] }),
131      Input({ key: 'scratch-note', label: 'Scratch note', value: note, placeholder: 'Synthetic notes only; 500 characters max', submitLabel: 'Save', onSubmit: async (value) => {
132        const text = value.slice(0, 500);
133        await $.store.set('scratch-note', text);
134        note = text;
135        $.ui.invalidate('ui.render');
136      } }),
137      Text({ children: [`Saved / loaded: ${note || '(empty)'}`] }),
138      Button({ key: 'forget-note', label: 'Delete saved note', onPress: async () => {
139        await $.store.delete('scratch-note'); note = ''; $.ui.invalidate('ui.render');
140      } }),
141    ];
142    return Box({ flexDirection: 'column', rowGap: 1, children: [
143      Text({ bold: true, children: ['CLAUDE MOD KITCHEN SINK'] }),
144      Text({ dimColor: true, children: [`Renderer: ${e.surface} · experimental learning lab`] }),
145      Box({ flexDirection: 'row', columnGap: 1, children: tabs }),
146      ...body,
147    ] });
148  });
149}
150