SLOPSHOPPER

grill

Grill-Assistent: mit /grill stellt Claude bis zu 5 Rueckfragen zu deinem Plan, eine pro Schritt als Band ueber dem Eingabefeld, mit Antwortoptionen…

newbandcommandtoastmodel
v0.1.0MITupdated 2026-10-06broening/claude-mods/grill
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · grill
› fix the failing auth test and add an audit log call ⏺ 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 › /grill ⎿ grill: Grill gestartet. ⟨Claude Code's own drawing⟩ ◆ Grill: denkt nach ... ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
⟨Claude Code's own drawing⟩ ◆ Grill: denkt nach ...
README

claude-mods

Mods für Claude Code. Jeder Mod ist ein kleines Plugin aus TypeScript-Hooks. Er ändert Claude Code von innen: ein Band über dem Eingabefeld, ein Seitenfenster, eigene Slash-Befehle oder eine Rückfrage vor Tool-Aufrufen.

Mods prüfen

Die Mods

ModWas er machtBefehl
cache-countdownZeigt über dem Prompt, wie lange der Prompt-Cache (1 Stunde) noch warm ist: grün, orange ab 15 Min, rot wenn kalt. Knopf Compact und Schalter Auto: verdichtet 2 Minuten vor kalt von selbst./compact-cache
blast-radiusHält gefährliche Shell-Befehle (Bash und PowerShell) an, zum Beispiel rm -rf, Remove-Item -Recurse, git reset --hard, git push --force, DROP TABLE. Fragt vor dem Ausführen nach. Standard aus./ward an/aus
next-stepsSchlägt nach jeder Antwort drei nächste Schritte vor. Ein Klick schickt den Schritt als Prompt./vorschlaege
todo-listArbeitsliste im Seitenfenster. Aufgaben einreihen, ohne Claude zu unterbrechen. Mit Auto arbeitet Claude sie nacheinander ab./todo
grillStellt bis zu 5 Rückfragen zu deinem Plan, eine nach der anderen, mit Antwortknöpfen und KI-Vorschlag./grill [thema]

Hinweis: next-steps und grill rufen das Modell zusätzlich auf ($.model.fork). Das kostet etwas Kontingent.

Voraussetzungen

  • Claude Code mit Mod-Unterstützung (getestet mit 2.1.287)
  • Git
  • Windows: PowerShell 5.1 oder neuer. macOS und Linux: siehe unten.

Installieren (neuer Rechner)

Unter Windows in PowerShell:

git clone https://github.com/broening/claude-mods.git "$env:USERPROFILE\.claude\mods"
& "$env:USERPROFILE\.claude\mods\install.ps1"

Danach Claude Code neu starten. Das Skript trägt alle Mod-Ordner in ~/.claude/settings.json unter env.CLAUDE_CODE_PLUGIN_DIRS ein. Andere Einstellungen bleiben. Eine Sicherung liegt danach in settings.json.bak.

Nur einzelne Mods:

& "$env:USERPROFILE\.claude\mods\install.ps1" -Only cache-countdown,blast-radius

macOS und Linux: Repo nach ~/.claude/mods klonen und in ~/.claude/settings.json eintragen (Trennzeichen :):

{
  "env": {
    "CLAUDE_CODE_PLUGIN_DIRS": "/home/<du>/.claude/mods/cache-countdown:/home/<du>/.claude/mods/blast-radius"
  }
}

Alternative: als Plugin-Marktplatz

Das Repo ist auch ein Claude-Code-Marktplatz (.claude-plugin/marketplace.json). In Claude Code:

/plugin marketplace add broening/claude-mods
/plugin install cache-countdown@claude-mods

Nicht beides gleichzeitig nutzen, sonst lädt ein Mod doppelt.

Aktualisieren

git -C "$env:USERPROFILE\.claude\mods" pull

Danach Claude Code neu starten. Kommt ein neuer Mod dazu, install.ps1 noch einmal ausführen.

Aufbau eines Mods

<mod>/
  .claude-plugin/plugin.json   Name, Version, Beschreibung
  hooks/hooks.json             { "modules": ["./register.tsx"] }
  hooks/register.tsx           die Hooks: export const register: Register = on => { ... }
  types/index.d.ts             Vertrag für den gespeicherten Zustand ($.state)
  tsconfig.json

.claude-plugin/types/ erzeugt Claude Code bei jedem Build selbst. Der Ordner ist nicht im Repo.

Entwickeln

  1. Mod in seinem Ordner ändern. In einer laufenden Sitzung lädt Claude Code ihn nach dem nächsten Turn neu.
  2. Prüfen:
   claude plugin validate .\cache-countdown
  1. version in plugin.json erhöhen und einen Eintrag in CHANGELOG.md schreiben.
  2. Committen und pushen. GitHub Actions prüft alle Mods bei jedem Push.

Neue Mods am einfachsten mit dem Claude-Code-Skill plugin-authoring bauen.

Lizenz

MIT

Source 2 files
hooks/register.tsx 300 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Frage } from '../types'
5
6// Grill-Assistent als Band UEBER dem Eingabefeld. Mit "/grill" (optional
7// "/grill <thema>") fragt das Plugin Claude selbst (ueber $.model.fork auf dem
8// laufenden Gespraech) nach bis zu 5 Rueckfragen, die den aktuellen Plan oder
9// die Entscheidung auf die Probe stellen. Das Band zeigt eine Frage nach der
10// anderen: Antwortoptionen, den KI-Vorschlag und ein Feld fuer eine eigene
11// Antwort. Nach der letzten Frage gehen alle Antworten gesammelt als Prompt an
12// Claude. Ein Fork erzeugt keinen neuen Zug. Nur pro Sitzung, nichts gespeichert.
13
14const GREEN = '#22c55e'
15const MAX_FRAGEN = 5
16const MAX_OPTIONEN = 4
17const LABEL_LEN = 70 // gekuerzt fuer die Anzeige im schmalen Band
18
19const FORK_PROMPT = [
20  'Stelle den aktuellen Plan bzw. die aktuelle Entscheidung aus diesem Gespraech',
21  'auf die Probe. Formuliere bis zu 5 kurze Rueckfragen, die Schwachstellen,',
22  'offene Annahmen und Risiken aufdecken. Die wichtigste Frage kommt zuerst.',
23  'Jede Frage hat 2 bis 4 konkrete Antwortoptionen, die sich gegenseitig',
24  'ausschliessen, und genau eine empfohlene Option als "vorschlag" (sie muss',
25  'wortgleich eine der Optionen sein).',
26  'Antworte NUR mit einem JSON-Array, ohne Text davor oder danach und ohne',
27  'Code-Zaeune. Jedes Element hat die Form',
28  '{ "frage": string, "optionen": string[], "vorschlag": string }.',
29  'Rufe keine Werkzeuge auf.',
30].join(' ')
31
32// Sitzungszustand. Nur pro Sitzung.
33const fragen = atom({ plugin: 'grill', key: 'fragen' } as const, [] as Frage[])
34const antworten = atom(
35  { plugin: 'grill', key: 'antworten' } as const,
36  [] as (string | null)[],
37)
38const index = atom({ plugin: 'grill', key: 'index' } as const, 0)
39const aktiv = atom({ plugin: 'grill', key: 'aktiv' } as const, false)
40const laden = atom({ plugin: 'grill', key: 'laden' } as const, false)
41const draft = atom({ plugin: 'grill', key: 'draft' } as const, '')
42
43// Zaehler, damit ein altes, langsames /grill ein neueres nicht ueberschreibt.
44let generation = 0
45
46// Zu lange Zeile fuers Band kuerzen.
47function cut(text: string): string {
48  return text.length > LABEL_LEN ? text.slice(0, LABEL_LEN - 3) + '...' : text
49}
50
51// Antwort des Forks in Fragen zerlegen: Code-Zaeune weg, vom ersten '[' bis zum
52// letzten ']' lesen, nur wohlgeformte Eintraege behalten.
53function parseFragen(text: string): Frage[] {
54  const clean = text.replace(/```[a-zA-Z]*/g, '')
55  const start = clean.indexOf('[')
56  const end = clean.lastIndexOf(']')
57  if (start < 0 || end <= start) return []
58
59  let raw: unknown
60  try {
61    raw = JSON.parse(clean.slice(start, end + 1))
62  } catch {
63    return []
64  }
65  if (!Array.isArray(raw)) return []
66
67  const out: Frage[] = []
68  for (const item of raw) {
69    if (!item || typeof item !== 'object') continue
70    const o = item as { frage?: unknown; optionen?: unknown; vorschlag?: unknown }
71    if (typeof o.frage !== 'string' || !o.frage.trim()) continue
72    const optionen = Array.isArray(o.optionen)
73      ? o.optionen
74          .filter((x): x is string => typeof x === 'string' && x.trim() !== '')
75          .map(x => x.trim())
76          .slice(0, MAX_OPTIONEN)
77      : []
78    out.push({
79      frage: o.frage.trim(),
80      optionen,
81      vorschlag: typeof o.vorschlag === 'string' ? o.vorschlag.trim() : '',
82    })
83    if (out.length >= MAX_FRAGEN) break
84  }
85
86  return out
87}
88
89// Sitzung komplett leeren und das Band ausblenden.
90async function clearSession($: EngineInterface) {
91  await update($, aktiv, () => false)
92  await update($, laden, () => false)
93  await update($, fragen, () => [])
94  await update($, antworten, () => [])
95  await update($, index, () => 0)
96  await update($, draft, () => '')
97}
98
99// Fragen erzeugen: Fork starten, Antwort zerlegen, Sitzung starten.
100async function startGrill($: EngineInterface, focus: string) {
101  const mine = ++generation
102  await update($, aktiv, () => true)
103  await update($, laden, () => true)
104  await update($, fragen, () => [])
105  await update($, antworten, () => [])
106  await update($, index, () => 0)
107  await update($, draft, () => '')
108
109  const prompt = focus
110    ? FORK_PROMPT + ' Konzentriere dich auf: ' + focus
111    : FORK_PROMPT
112
113  let list: Frage[] = []
114  let grund = 'Grill: keine Fragen erhalten.'
115  try {
116    const r = await $.model.fork({ prompt })
117    if (r.isAnswered) {
118      list = parseFragen(r.text)
119    } else if (r.reason === 'nothing-to-fork') {
120      if (focus) {
121        // Kein Gespraech zum Hinterfragen, aber ein Thema: direkt dazu fragen
122        // (ohne Verlauf, per $.model.complete).
123        const model = (await $.session.model()) || 'sonnet'
124        const direct = await $.model.complete({
125          model,
126          prompt: 'Thema: ' + focus + '\n\n' + FORK_PROMPT,
127          maxTokens: 1024,
128        })
129        if (direct.isAnswered) list = parseFragen(direct.text)
130      } else {
131        grund =
132          'Grill: erst ein Gespraech fuehren, oder "/grill <thema>" mit einem Thema nutzen.'
133      }
134    }
135  } catch {
136    // grund bleibt
137  }
138
139  if (mine !== generation) return
140
141  if (list.length === 0) {
142    await clearSession($)
143    $.ui.toast(grund)
144
145    return
146  }
147
148  await update($, fragen, () => list)
149  await update($, antworten, () => list.map(() => null))
150  await update($, index, () => 0)
151  await update($, laden, () => false)
152}
153
154// Alle Antworten sammeln, Sitzung leeren, als Prompt an Claude schicken.
155async function finish($: EngineInterface) {
156  const list = await read($, fragen)
157  const answers = await read($, antworten)
158
159  const lines = ['Hier meine Antworten auf deine Rueckfragen:']
160  list.forEach((f, i) => {
161    lines.push('- ' + f.frage + ' => ' + (answers[i] ?? '(offen)'))
162  })
163  const text = lines.join('\n')
164
165  generation++
166  await clearSession($)
167  await $.prompt.submit({ text, asUser: true })
168}
169
170// Antwort zu Frage i festhalten und weiterschalten (nach der letzten: fertig).
171async function antwort($: EngineInterface, i: number, text: string) {
172  const list = await read($, fragen)
173  await update($, antworten, a => a.map((x, k) => (k === i ? text : x)))
174  await update($, draft, () => '')
175
176  if (i >= list.length - 1) {
177    await finish($)
178  } else {
179    await update($, index, () => i + 1)
180  }
181}
182
183// Abbrechen: Sitzung leeren, laufenden Fork verwerfen.
184async function abbrechen($: EngineInterface) {
185  generation++
186  await clearSession($)
187  $.ui.toast('Grill abgebrochen.')
188}
189
190export const register: Register = on => {
191  on('session.start', async ($, e, next) => {
192    await $.command.register({
193      name: 'grill',
194      description:
195        'Grill: Claude stellt dir bis zu 5 Rueckfragen zu deinem Plan, mit "/grill <thema>" mit Schwerpunkt',
196    })
197
198    return next(e)
199  })
200
201  on('command.run', { command: 'grill' }, async ($, e) => {
202    // Nicht blockieren: Fork im Hintergrund, das Band zeigt "denkt nach".
203    void startGrill($, e.args.trim())
204
205    return { text: 'Grill gestartet.' }
206  })
207
208  // Band ueber dem Eingabefeld. Es gibt nur EIN Band ("one instance"), darum
209  // reichen wir das darunterliegende Band (below) durch und haengen unseres an.
210  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
211    const below = await next(e)
212    if (!(await read($, aktiv)) || e.props?.hasSurvey) return below
213
214    const { Box, Text, Button, Input } = $.ui.resolve(e)
215
216    if (await read($, laden)) {
217      return (
218        <Box flexDirection="column">
219          {below}
220          <Box gap={1}>
221            <Text dimColor>{'\u25C6'} Grill: denkt nach ...</Text>
222          </Box>
223        </Box>
224      )
225    }
226
227    const list = await read($, fragen)
228    const answers = await read($, antworten)
229    const at = await read($, index)
230    const draftText = await read($, draft)
231
232    const frage = list[at]
233    if (!frage) return below
234
235    const isLast = at >= list.length - 1
236    const given = answers[at]
237
238    return (
239      <Box flexDirection="column">
240        {below}
241        <Text bold color={GREEN}>
242          Frage {at + 1}/{list.length}
243        </Text>
244        <Text bold>{frage.frage}</Text>
245        {given != null && <Text dimColor>Deine Antwort: {cut(given)}</Text>}
246        {frage.optionen.map((option, i) => (
247          <Box key={'opt-' + at + '-' + i} gap={1} alignItems="flex-start">
248            <Text dimColor>{i + 1}</Text>
249            <Button
250              key={'opt-btn-' + at + '-' + i}
251              label={cut(option)}
252              onPress={() => void antwort($, at, option)}
253            />
254          </Box>
255        ))}
256        {frage.vorschlag !== '' && (
257          <Box gap={1} alignItems="flex-start">
258            <Text bold color={GREEN}>
259              {'\u2605'}
260            </Text>
261            <Button
262              key={'ki-' + at}
263              label={cut('KI-Vorschlag: ' + frage.vorschlag)}
264              onPress={() => void antwort($, at, frage.vorschlag)}
265            />
266          </Box>
267        )}
268        <Input
269          key={'own-' + at}
270          placeholder="eigene Antwort, Enter"
271          submitLabel="antworten"
272          autoFocus
273          value={draftText}
274          onInput={value => void update($, draft, () => value)}
275          onSubmit={value => {
276            const text = value.trim()
277            if (text) void antwort($, at, text)
278          }}
279        />
280        <Box gap={1}>
281          <Button
282            key="zurueck"
283            label="zurueck"
284            onPress={() => void update($, index, i => Math.max(0, i - 1))}
285          />
286          <Button
287            key="weiter"
288            label={isLast ? 'fertig' : 'weiter'}
289            onPress={() => {
290              if (isLast) void finish($)
291              else void update($, index, i => i + 1)
292            }}
293          />
294          <Button key="abbrechen" label="abbrechen" onPress={() => void abbrechen($)} />
295        </Box>
296      </Box>
297    )
298  })
299}
300
types/index.d.ts 19 lines
1export type Frage = {
2  frage: string
3  optionen: string[]
4  vorschlag: string
5}
6
7declare module 'claude-code' {
8  interface PluginState {
9    'grill': {
10      fragen: Frage[]
11      antworten: (string | null)[]
12      index: number
13      aktiv: boolean
14      laden: boolean
15      draft: string
16    }
17  }
18}
19