Zeigt nach jeder Antwort die stillen Annahmen von Claude (Band ueber dem Eingabefeld); richtig/falsch markieren, falsche gehen als Korrektur an Claude…

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.
| Mod | Was er macht | Befehl |
|---|---|---|
| cache-countdown | Zeigt ü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-radius | Hä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-steps | Schlägt nach jeder Antwort drei nächste Schritte vor. Ein Klick schickt den Schritt als Prompt. | /vorschlaege |
| todo-list | Arbeitsliste im Seitenfenster. Aufgaben einreihen, ohne Claude zu unterbrechen. Mit Auto arbeitet Claude sie nacheinander ab. | /todo |
| annahmen | Zeigt nach jeder Antwort die Annahmen, die Claude still getroffen hat. Du markierst sie als richtig oder falsch. Korrektur senden schickt die falschen als neuen Auftrag. | /annahmen an/aus |
| prompt-boost | Knopf Prompt verbessern über dem Eingabefeld. Schreibt deinen Entwurf besser um (passende Skills, offene Fragen, Zielbild). Ersetzt nur den Entwurf, sendet nichts. rueckgaengig holt den alten Text zurück. | /verbessern an/aus |
| sitzungs-waechter | Warnt über dem Prompt, wenn eine zweite Claude-Code-Sitzung im selben Ordner läuft. Dann können sich Dateien überschreiben. ok blendet die Warnung aus. | – |
| grill | Stellt bis zu 5 Rückfragen zu deinem Plan, eine nach der anderen, mit Antwortknöpfen und KI-Vorschlag. | /grill [thema] |
Hinweis: next-steps, annahmen, prompt-boost und grill rufen das Modell zusätzlich auf ($.model.fork). Das kostet etwas Kontingent. next-steps und annahmen teilen sich eine Anfrage pro Antwort; annahmen braucht dafür next-steps.
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"
}
}
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.
git -C "$env:USERPROFILE\.claude\mods" pull
Danach Claude Code neu starten. Kommt ein neuer Mod dazu, install.ps1 noch einmal ausführen.
<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.
claude plugin validate .\cache-countdown
version in plugin.json erhöhen und einen Eintrag in CHANGELOG.md schreiben.Neue Mods am einfachsten mit dem Claude-Code-Skill plugin-authoring bauen.
hooks/register.tsx 303 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3import type { Assumption, Mark } from '../types'
4
5// Annahmen-Pruefer als Band UEBER dem Eingabefeld. Ist Claude mit einer Antwort
6// fertig, fragt der Mod Claude selbst (ueber $.model.fork auf dem laufenden
7// Gespraech) nach den Annahmen, die es still getroffen hat. Das Band zeigt erst
8// nur die Zahl; "pruefen" klappt die Liste auf. Jede Annahme bekommt richtig
9// oder falsch. "Korrektur senden" schickt die falschen (plus Notiz) als neuen
10// Auftrag an Claude. Ein Fork erzeugt keinen neuen Zug, also keine Schleife.
11// Mit "/annahmen" an/aus.
12//
13// Ist next-steps an, fragt annahmen NICHT selbst: next-steps holt Schritte und
14// Annahmen mit einem einzigen Fork und legt die Annahmen in "next-steps.shared".
15// annahmen hoert auf diese Schreibvorgaenge (state.set-Hook) und uebernimmt die
16// Liste. Ist next-steps aus oder nicht geladen, fragt annahmen selbst.
17
18const GREEN = '#22c55e'
19const RED = '#ef4444'
20const MAX_ITEMS = 6
21const MAX_LEN = 200 // volle Laenge beim Abschicken
22const BAND_LEN = 90 // gekuerzt fuer die Anzeige im Band
23
24const FORK_PROMPT = [
25 'Pruefe deine letzte Antwort und die Arbeit dafuer.',
26 'Liste die Annahmen, die du STILL getroffen hast: kleine Entscheidungen, die',
27 'der Nutzer nicht ausdruecklich vorgegeben hat (z.B. Namen, Orte, Standardwerte,',
28 'Reihenfolge, was weggelassen wurde, wie etwas gemeint war).',
29 'Nur Annahmen, die falsch sein koennten und die der Nutzer pruefen sollte.',
30 `Hoechstens ${MAX_ITEMS}. Jede Annahme ist ein kurzer deutscher Satz in einer`,
31 'Zeile (hoechstens etwa 120 Zeichen), ohne Nummer, ohne Einleitung.',
32 'Gibt es keine solche Annahme, antworte nur mit dem Wort KEINE.',
33 'Nutze keine Werkzeuge.',
34].join(' ')
35
36const items = atom({ plugin: 'annahmen', key: 'items' } as const, [] as Assumption[])
37const expanded = atom({ plugin: 'annahmen', key: 'expanded' } as const, false)
38const loading = atom({ plugin: 'annahmen', key: 'loading' } as const, false)
39const enabled = atom({ plugin: 'annahmen', key: 'enabled' } as const, true)
40const note = atom({ plugin: 'annahmen', key: 'note' } as const, '')
41// Auf welche next-steps-Erzeugung (gen) warten wir gerade? -1 = auf keine.
42const pending = atom({ plugin: 'annahmen', key: 'pending' } as const, -1)
43
44const nextStepsEnabled = { plugin: 'next-steps', key: 'enabled' } as const
45const nextStepsShared = { plugin: 'next-steps', key: 'shared' } as const
46
47// Zaehler, damit eine alte, langsame Antwort eine neuere nicht ueberschreibt.
48let generation = 0
49
50function cut(text: string): string {
51 return text.length > BAND_LEN ? text.slice(0, BAND_LEN - 3) + '...' : text
52}
53
54// Antwort des Forks in bis zu MAX_ITEMS Zeilen zerlegen: Nummern und
55// Aufzaehlungszeichen vorne weg, leere und doppelte Zeilen raus. "KEINE" = leer.
56export function parseAssumptions(text: string): string[] {
57 const out: string[] = []
58 for (const line of text.split(/\r?\n/)) {
59 const clean = line
60 .replace(/^\s*(?:[-*•–—]+|\d+\s*[.):])\s*/, '')
61 .replace(/^(?:annahme\s*\d*\s*:\s*)/i, '')
62 .replace(/^[`"']+|[`"']+$/g, '')
63 .trim()
64 if (!clean || /^keine\.?$/i.test(clean) || out.includes(clean)) continue
65 out.push(clean.length > MAX_LEN ? clean.slice(0, MAX_LEN - 3) + '...' : clean)
66 if (out.length >= MAX_ITEMS) break
67 }
68
69 return out
70}
71
72// Text des Korrektur-Auftrags bauen. Leer, wenn nichts falsch markiert ist.
73export function buildCorrection(list: Assumption[], extra: string): string {
74 const wrong = list.filter(a => a.mark === 'falsch')
75 if (wrong.length === 0) return ''
76 const right = list.filter(a => a.mark === 'richtig')
77 const lines = ['Diese Annahmen von dir waren falsch. Bitte korrigiere sie:']
78 for (const a of wrong) lines.push('- ' + a.text)
79 if (right.length > 0) {
80 lines.push('', 'Diese Annahmen waren richtig, behalte sie:')
81 for (const a of right) lines.push('- ' + a.text)
82 }
83 const n = extra.trim()
84 if (n) lines.push('', 'Hinweis von mir: ' + n)
85 lines.push('', 'Wenn dir unklar ist, wie es richtig waere, frag mich zuerst.')
86
87 return lines.join('\n')
88}
89
90async function clear($: EngineInterface) {
91 generation++
92 await update($, pending, () => -1)
93 await update($, items, () => [])
94 await update($, expanded, () => false)
95 await update($, loading, () => false)
96 await update($, note, () => '')
97}
98
99async function generate($: EngineInterface) {
100 const mine = ++generation
101 await update($, loading, () => true)
102 await update($, items, () => [])
103 await update($, expanded, () => false)
104
105 let found: string[] = []
106 try {
107 const r = await $.model.fork({ prompt: FORK_PROMPT })
108 if (r.isAnswered) {
109 found = parseAssumptions(r.text)
110 } else if (r.reason === 'api-error') {
111 $.ui.toast('Annahmen: Fehler bei der Anfrage.')
112 }
113 } catch {
114 $.ui.toast('Annahmen: Anfrage fehlgeschlagen.')
115 }
116
117 if (mine !== generation) return
118 await update($, items, () => found.map(text => ({ text, mark: 'offen' as Mark })))
119 await update($, loading, () => false)
120}
121
122// next-steps hat "shared" geschrieben: Start (loading) merken, fertige Liste
123// der gemerkten Erzeugung uebernehmen. Alles andere (alte gen, nach "weg" oder
124// neuem Prompt) bleibt liegen.
125async function adopt(
126 $: EngineInterface,
127 value: { gen: number; loading: boolean; assumptions: string[] },
128) {
129 if (!(await read($, enabled))) return
130 if (value.loading) {
131 generation++ // eigener Fork, falls einer laeuft, zaehlt nicht mehr
132 await update($, pending, () => value.gen)
133 await update($, loading, () => true)
134 await update($, items, () => [])
135 await update($, expanded, () => false)
136 return
137 }
138 if ((await read($, pending)) !== value.gen) return
139 await update($, pending, () => -1)
140 await update($, items, () => value.assumptions.map(text => ({ text, mark: 'offen' as Mark })))
141 await update($, loading, () => false)
142}
143
144// Laeuft next-steps (und holt die Annahmen mit)?
145async function sharedFork($: EngineInterface): Promise<boolean> {
146 try {
147 const { value } = await $.state.get(nextStepsEnabled)
148 return value === true
149 } catch {
150 return false
151 }
152}
153
154async function setMark($: EngineInterface, index: number, mark: Mark) {
155 await update($, items, list =>
156 list.map((a, i) => (i === index ? { ...a, mark: a.mark === mark ? 'offen' : mark } : a)),
157 )
158}
159
160async function sendCorrection($: EngineInterface) {
161 const text = buildCorrection(await read($, items), await read($, note))
162 if (!text) return
163 await clear($)
164 await $.prompt.submit({ text, asUser: true })
165}
166
167async function toggleEnabled($: EngineInterface) {
168 const next = !(await read($, enabled))
169 await update($, enabled, () => next)
170 await $.store.set('enabled', next)
171 if (!next) await clear($)
172}
173
174export const register: Register = on => {
175 on('session.start', async ($, e, next) => {
176 const saved = await $.store.get('enabled')
177 await update($, enabled, () => saved !== false)
178 await $.command.register({
179 name: 'annahmen',
180 description: 'Annahmen-Band ueber dem Eingabefeld an- oder ausschalten',
181 })
182
183 return next(e)
184 })
185
186 on('command.run', { command: 'annahmen' }, async $ => {
187 await toggleEnabled($)
188 const isOn = await read($, enabled)
189
190 return { text: isOn ? 'Annahmen-Pruefer ist an.' : 'Annahmen-Pruefer ist aus.' }
191 })
192
193 // Neuer Prompt: alte Annahmen gehoeren zur alten Antwort, also weg.
194 on('prompt.submit', async ($, e, next) => {
195 await clear($)
196
197 return next(e)
198 })
199
200 on('turn.complete', async ($, e, next) => {
201 if (e.agentId || e.reason !== 'answer') return next(e)
202 if (!(await read($, enabled))) return next(e)
203 if (!(await sharedFork($))) void generate($)
204
205 return next(e)
206 })
207
208 // next-steps schreibt seine Fork-Ergebnisse: Annahmen daraus uebernehmen.
209 on('state.set', nextStepsShared, async ($, e, next) => {
210 const r = await next(e)
211 // Hooks auf einem $-Aufruf sehen die Antwort als { value }, sonst direkt.
212 const res = r as unknown as { isSet?: boolean; value?: { isSet?: boolean } }
213 if (res.value?.isSet ?? res.isSet) await adopt($, e.value)
214
215 return r
216 })
217
218 // Nur EIN Band ueber dem Prompt: darunterliegendes Band durchreichen, unseres
219 // anhaengen.
220 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
221 const below = await next(e)
222 if (e.props?.hasSurvey) return below
223 if (!(await read($, enabled))) return below
224
225 const busy = await read($, loading)
226 const list = await read($, items)
227 if (!busy && list.length === 0) return below
228
229 const el = $.ui.resolve(e)
230 const { Box, Text, Button } = el
231 // Nicht jede Oberflaeche hat ein Eingabefeld; dann ohne Notiz.
232 const Input = 'Input' in el ? el.Input : undefined
233
234 if (busy) {
235 return (
236 <Box flexDirection="column">
237 {below}
238 <Text dimColor>{'◇'} Annahmen: sucht ...</Text>
239 </Box>
240 )
241 }
242
243 const open = await read($, expanded)
244 const wrong = list.filter(a => a.mark === 'falsch').length
245
246 if (!open) {
247 return (
248 <Box flexDirection="column">
249 {below}
250 <Box gap={1}>
251 <Text color="yellow">{'◇'}</Text>
252 <Text>
253 {list.length} {list.length === 1 ? 'Annahme' : 'Annahmen'}
254 </Text>
255 <Button key="open" label="pruefen" onPress={() => void update($, expanded, () => true)} />
256 <Button key="dismiss" label="weg" onPress={() => void clear($)} />
257 </Box>
258 </Box>
259 )
260 }
261
262 const noteText = await read($, note)
263
264 return (
265 <Box flexDirection="column">
266 {below}
267 <Text bold color="yellow">
268 {'◇'} Annahmen von Claude: richtig oder falsch?
269 </Text>
270 {list.map((a, i) => (
271 <Box key={'a-' + i} gap={1} alignItems="flex-start">
272 <Text color={a.mark === 'richtig' ? GREEN : a.mark === 'falsch' ? RED : undefined} dimColor={a.mark === 'offen'}>
273 {a.mark === 'richtig' ? '✓' : a.mark === 'falsch' ? '✗' : String(i + 1)}
274 </Text>
275 <Text>{cut(a.text)}</Text>
276 <Button key={'ok-' + i} label="richtig" onPress={() => void setMark($, i, 'richtig')} />
277 <Button key={'no-' + i} label="falsch" onPress={() => void setMark($, i, 'falsch')} />
278 </Box>
279 ))}
280 {Input ? (
281 <Input
282 key="note"
283 placeholder="Notiz an Claude (optional)"
284 submitLabel="Korrektur senden"
285 value={noteText}
286 onInput={(value: string) => void update($, note, () => value)}
287 onSubmit={(value: string) => {
288 void update($, note, () => value).then(() => sendCorrection($))
289 }}
290 />
291 ) : null}
292 <Box gap={1}>
293 {wrong > 0 ? (
294 <Button key="send" label={'Korrektur senden (' + wrong + ')'} onPress={() => void sendCorrection($)} />
295 ) : null}
296 <Button key="close" label="zuklappen" onPress={() => void update($, expanded, () => false)} />
297 <Button key="done" label="alles gut" onPress={() => void clear($)} />
298 </Box>
299 </Box>
300 )
301 })
302}
303types/index.d.ts 19 lines1export type Mark = 'offen' | 'richtig' | 'falsch'
2export interface Assumption {
3 text: string
4 mark: Mark
5}
6
7declare module 'claude-code' {
8 interface PluginState {
9 annahmen: {
10 items: Assumption[]
11 expanded: boolean
12 loading: boolean
13 enabled: boolean
14 note: string
15 pending: number
16 }
17 }
18}
19