Schlaegt nach jeder fertigen Antwort drei naechste Schritte vor (Band ueber dem Eingabefeld); ein Klick schickt den Schritt als Prompt. Ein/Aus-Schalter…

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 |
| 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 und grill rufen das Modell zusätzlich auf ($.model.fork). Das kostet etwas Kontingent.
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 185 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4// Naechste Schritte als Band UEBER dem Eingabefeld. Ist Claude mit einer Antwort
5// fertig, fragt das Plugin Claude selbst (ueber $.model.fork auf dem laufenden
6// Gespraech) nach den drei wahrscheinlichsten naechsten Schritten. Prio 1 ist
7// der wahrscheinlichste und gruen, 2 und 3 sind Alternativen. Ein Klick schickt
8// den Schritt als naechsten Prompt. Ein Fork erzeugt keinen neuen Zug, die
9// Erzeugung laeuft also nie in einer Schleife. Mit "/vorschlaege" an/aus.
10
11const GREEN = '#22c55e'
12const MAX_STEPS = 3
13const MAX_LEN = 160 // volle Laenge beim Abschicken
14const BAND_LEN = 72 // gekuerzt fuer die Anzeige im schmalen Band
15
16const FORK_PROMPT = [
17 'Nenne genau 3 realistische naechste Schritte, um diese Arbeit fortzusetzen.',
18 'Der wahrscheinlichste Schritt kommt zuerst, dann die Alternativen.',
19 'Jeder Schritt ist eine kurze Anweisung im Imperativ, die ein Entwickler als',
20 'Naechstes an dich schicken koennte (eine Zeile, hoechstens etwa 100 Zeichen).',
21 'Antworte NUR mit diesen 3 Zeilen: eine Zeile pro Schritt, ohne Nummerierung,',
22 'ohne Aufzaehlungszeichen, ohne Einleitung, ohne Erklaerung, ohne Werkzeuge.',
23].join(' ')
24
25// Aktuelle Vorschlaege. Nur pro Sitzung.
26const suggestions = atom(
27 { plugin: 'next-steps', key: 'suggestions' } as const,
28 [] as string[],
29)
30// Laeuft gerade ein Fork?
31const loading = atom({ plugin: 'next-steps', key: 'loading' } as const, false)
32// Automatisch Vorschlaege machen? Wird gespeichert.
33const enabled = atom({ plugin: 'next-steps', key: 'enabled' } as const, true)
34
35// Zaehler, damit eine alte, langsame Antwort eine neuere nicht ueberschreibt.
36let generation = 0
37
38// Zu lange Zeile fuers Band kuerzen.
39function cut(text: string): string {
40 return text.length > BAND_LEN ? text.slice(0, BAND_LEN - 3) + '...' : text
41}
42
43// Antwort des Forks in bis zu 3 kurze Zeilen zerlegen: Nummern, Spiegelstriche
44// und Aufzaehlungszeichen vorne weg, leere Zeilen raus, Doppelte raus.
45function parseSteps(text: string): string[] {
46 const out: string[] = []
47 for (const line of text.split(/\r?\n/)) {
48 const clean = line
49 .replace(/^\s*(?:[-*•–—]+|\d+\s*[.):])\s*/, '')
50 .replace(/^[`"']+|[`"']+$/g, '')
51 .trim()
52 if (!clean || out.includes(clean)) continue
53 out.push(clean.length > MAX_LEN ? clean.slice(0, MAX_LEN - 3) + '...' : clean)
54 if (out.length >= MAX_STEPS) break
55 }
56
57 return out
58}
59
60// Vorschlaege erzeugen: Fork starten, Antwort zerlegen, ablegen.
61async function generate($: EngineInterface) {
62 const mine = ++generation
63 await update($, loading, () => true)
64 await update($, suggestions, () => [])
65
66 let steps: string[] = []
67 try {
68 const r = await $.model.fork({ prompt: FORK_PROMPT })
69 if (r.isAnswered) {
70 steps = parseSteps(r.text)
71 } else if (r.reason === 'api-error') {
72 $.ui.toast('Vorschlaege: Fehler bei der Anfrage.')
73 }
74 } catch {
75 $.ui.toast('Vorschlaege: Anfrage fehlgeschlagen.')
76 }
77
78 if (mine !== generation) return
79 await update($, suggestions, () => steps)
80 await update($, loading, () => false)
81}
82
83// Ein/Aus umschalten: Atom und Store zusammen.
84async function toggleEnabled($: EngineInterface) {
85 const next = !(await read($, enabled))
86 await update($, enabled, () => next)
87 await $.store.set('enabled', next)
88 $.ui.toast(next ? 'Vorschlaege an' : 'Vorschlaege aus')
89 if (!next) {
90 generation++
91 await update($, loading, () => false)
92 await update($, suggestions, () => [])
93 }
94}
95
96// Vorschlag anklicken: als Prompt schicken, Liste leeren.
97async function pick($: EngineInterface, text: string) {
98 generation++
99 await update($, suggestions, () => [])
100 await update($, loading, () => false)
101 await $.prompt.submit({ text, asUser: true })
102}
103
104export const register: Register = on => {
105 on('session.start', async ($, e, next) => {
106 const saved = await $.store.get('enabled')
107 await update($, enabled, () => saved !== false)
108 await $.command.register({
109 name: 'vorschlaege',
110 description: 'Vorschlaege-Band ueber dem Eingabefeld an- oder ausschalten',
111 })
112
113 return next(e)
114 })
115
116 on('command.run', { command: 'vorschlaege' }, async $ => {
117 await toggleEnabled($)
118 const isOn = await read($, enabled)
119
120 return { text: isOn ? 'Vorschlaege sind an.' : 'Vorschlaege sind aus.' }
121 })
122
123 on('turn.complete', async ($, e, next) => {
124 // Nur die Hauptschleife zaehlt, nur erfolgreiche Antworten.
125 if (e.agentId || e.reason !== 'answer') return next(e)
126 if (!(await read($, enabled))) return next(e)
127
128 // Nicht blockieren: Erzeugung starten und den Zug sofort weitergeben.
129 void generate($)
130
131 return next(e)
132 })
133
134 // Band ueber dem Eingabefeld. Nur EIN Band ("one instance"), darum reichen wir
135 // das darunterliegende Band (below) durch und haengen unseres an.
136 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
137 const below = await next(e)
138 if (e.props?.hasSurvey) return below
139 if (!(await read($, enabled))) return below
140
141 const busy = await read($, loading)
142 const list = await read($, suggestions)
143 if (!busy && list.length === 0) return below
144
145 const { Box, Text, Button } = $.ui.resolve(e)
146
147 if (busy) {
148 return (
149 <Box flexDirection="column">
150 {below}
151 <Box gap={1}>
152 <Text dimColor>{'◆'} Vorschlaege: denkt nach ...</Text>
153 </Box>
154 </Box>
155 )
156 }
157
158 return (
159 <Box flexDirection="column">
160 {below}
161 {list.map((text, i) => (
162 <Box key={'s-' + i} gap={1} alignItems="flex-start">
163 {i === 0 ? (
164 <Text bold color={GREEN}>
165 1
166 </Text>
167 ) : (
168 <Text dimColor>{i + 1}</Text>
169 )}
170 <Button
171 key={'pick-' + i}
172 label={cut(text)}
173 onPress={() => void pick($, text)}
174 />
175 </Box>
176 ))}
177 <Box gap={1}>
178 <Button key="again" label="neu" onPress={() => void generate($)} />
179 <Button key="off" label="aus" onPress={() => void toggleEnabled($)} />
180 </Box>
181 </Box>
182 )
183 })
184}
185types/index.d.ts 10 lines1declare module 'claude-code' {
2 interface PluginState {
3 'next-steps': {
4 suggestions: string[]
5 loading: boolean
6 enabled: boolean
7 }
8 }
9}
10