Arbeitsliste im Seitenfenster (/todo): Aufgaben einreihen, ohne Claude zu unterbrechen; mit Auto arbeitet Claude sie nacheinander ab (z.B. ueber Nacht)…

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 372 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Task } from '../types'
5
6// Arbeitsliste im Seitenfenster. Dan reiht Aufgaben ein, ohne Claude bei der
7// laufenden Arbeit zu unterbrechen (Eingabefeld oder "/todo <text>").
8//
9// EIN Schalter: Automatik.
10// Auto an = die Liste wird von allein abgearbeitet, Aufgabe fuer Aufgabe
11// (z.B. ueber Nacht). Nach jeder fertigen Antwort startet die
12// naechste offene Aufgabe.
13// Auto aus = es laeuft NICHTS von selbst. Nur der ▶-Knopf startet genau eine
14// Aufgabe; danach ist Schluss, es wird nicht weitergemacht.
15//
16// Jede gestartete Aufgabe geht als eigener Prompt an Claude; ist die Antwort
17// fertig, wird die Aufgabe abgehakt. Erledigtes bleibt unten sichtbar. Die
18// Liste bleibt ueber Sitzungen hinweg erhalten.
19
20const PANE = 'todo-list'
21const PANE_TITLE = 'Arbeitsliste'
22const GREEN = '#22c55e'
23
24// Aufgaben: Atom (zeichnet das Fenster neu) + Store (bleibt dauerhaft).
25const tasks = atom({ plugin: 'todo-list', key: 'tasks' } as const, [] as Task[])
26// Die Aufgabe, an der Claude gerade arbeitet. Nur pro Sitzung.
27const activeId = atom(
28 { plugin: 'todo-list', key: 'activeId' } as const,
29 null as string | null,
30)
31// Automatik: Liste von allein abarbeiten. Wird gespeichert.
32const auto = atom({ plugin: 'todo-list', key: 'auto' } as const, false)
33// Welche Aufgabe wird gerade im Feld bearbeitet? Nur pro Sitzung.
34const editingId = atom(
35 { plugin: 'todo-list', key: 'editingId' } as const,
36 null as string | null,
37)
38// Aktueller Text im Eingabefeld. Nach dem Einreihen wird er geleert.
39const draft = atom({ plugin: 'todo-list', key: 'draft' } as const, '')
40
41// Sicherheit gegen Endlosschleifen: hoechstens so viele Aufgaben am Stueck
42// automatisch starten, dann Auto abschalten. Zaehler pro Sitzung.
43const AUTO_LIMIT = 25
44let autoRuns = 0
45
46// Prueft beim Laden aus dem Store, dass wirklich eine Liste gueltiger Aufgaben
47// da ist. Alte Aufgaben ohne createdAt/doneAt bekommen Standardwerte.
48function cleanTasks(raw: unknown, now: number): Task[] {
49 if (!Array.isArray(raw)) return []
50
51 const out: Task[] = []
52 for (const t of raw) {
53 if (
54 !t ||
55 typeof t !== 'object' ||
56 typeof (t as Task).id !== 'string' ||
57 typeof (t as Task).text !== 'string' ||
58 typeof (t as Task).done !== 'boolean'
59 ) {
60 continue
61 }
62 const one = t as Partial<Task> & Pick<Task, 'id' | 'text' | 'done'>
63 out.push({
64 id: one.id,
65 text: one.text,
66 done: one.done,
67 createdAt: typeof one.createdAt === 'number' ? one.createdAt : now,
68 doneAt:
69 typeof one.doneAt === 'number' ? one.doneAt : one.done ? now : null,
70 })
71 }
72
73 return out
74}
75
76// Liste aendern: Atom und Store zusammen, damit nichts auseinanderlaeuft.
77async function setTasks($: EngineInterface, fn: (list: Task[]) => Task[]) {
78 const next = fn(await read($, tasks))
79 await update($, tasks, () => next)
80 await $.store.set('tasks', next)
81
82 return next
83}
84
85// Neue Aufgabe hinten anreihen. Schickt nie einen Prompt, stoert Claude also nicht.
86async function addTask($: EngineInterface, raw: string) {
87 const text = raw.trim()
88 if (!text) return
89 const now = await $.clock.now()
90 const id = `${now}-${Math.random().toString(36).slice(2)}`
91 await setTasks($, list => [
92 ...list,
93 { id, text, done: false, createdAt: now, doneAt: null },
94 ])
95 autoRuns = 0 // neue Arbeit -> Sicherheits-Zaehler zuruecksetzen
96 $.ui.toast('Eingereiht')
97}
98
99// Bearbeiteten Text einer Aufgabe speichern. Leerer Text: nur das Feld zu.
100async function saveEdit($: EngineInterface, id: string, raw: string) {
101 const text = raw.trim()
102 await update($, editingId, () => null)
103 if (!text) return
104 await setTasks($, list =>
105 list.map(t => (t.id === id ? { ...t, text } : t)),
106 )
107}
108
109// Aufgabe von Hand ab- oder wieder anhaken (Kaestchen).
110async function toggleDone($: EngineInterface, id: string) {
111 const now = await $.clock.now()
112 await setTasks($, list =>
113 list.map(t =>
114 t.id === id
115 ? { ...t, done: !t.done, doneAt: !t.done ? now : null }
116 : t,
117 ),
118 )
119}
120
121// Eine Aufgabe als aktuelle setzen und an Claude schicken.
122async function startTask($: EngineInterface, task: Task) {
123 await update($, activeId, () => task.id)
124 await $.prompt.submit({ text: task.text })
125}
126
127// Naechste offene Aufgabe starten, wenn eine da ist. Gibt true zurueck, wenn
128// gestartet wurde.
129async function startNext($: EngineInterface): Promise<boolean> {
130 const first = (await read($, tasks)).find(t => !t.done)
131 if (!first) return false
132 await startTask($, first)
133
134 return true
135}
136
137// Automatik umschalten. Beim Einschalten die erste offene Aufgabe starten,
138// wenn gerade nichts laeuft.
139async function toggleAuto($: EngineInterface) {
140 const next = !(await read($, auto))
141 await update($, auto, () => next)
142 await $.store.set('auto', next)
143 $.ui.toast(next ? 'Automatik an' : 'Automatik aus')
144 if (next) {
145 autoRuns = 0 // frischer Lauf -> Sicherheits-Zaehler zuruecksetzen
146 if ((await read($, activeId)) === null) await startNext($)
147 }
148}
149
150export const register: Register = on => {
151 on('session.start', async ($, e, next) => {
152 const now = await $.clock.now()
153 const saved = cleanTasks(await $.store.get('tasks'), now)
154 await update($, tasks, () => saved)
155 const savedAuto = (await $.store.get('auto')) === true
156 await update($, auto, () => savedAuto)
157 await $.command.register({
158 name: 'todo',
159 description:
160 'Arbeitsliste: ohne Text oeffnen, mit "/todo <text>" eine Aufgabe einreihen',
161 })
162 // Fenster NICHT automatisch oeffnen - erscheint erst auf "/todo".
163
164 return next(e)
165 })
166
167 on('command.run', { command: 'todo' }, async ($, e) => {
168 const text = e.args.trim()
169 if (!text) {
170 await $.ui.open({ id: PANE, title: PANE_TITLE })
171
172 return { text: 'Arbeitsliste geoeffnet.' }
173 }
174 await addTask($, text)
175 await $.ui.open({ id: PANE, title: PANE_TITLE })
176
177 return { text: 'Eingereiht: ' + text }
178 })
179
180 on('turn.complete', async ($, e, next) => {
181 // Nur die Hauptschleife zaehlt, nicht Subagenten.
182 if (e.agentId) return next(e)
183
184 const current = await read($, activeId)
185 // War keine Listen-Aufgabe aktiv, macht die Liste gar nichts (kein
186 // Selbstlauf, keine Nachfrage).
187 if (current === null) return next(e)
188
189 // Unterbrochen oder Fehler: Aufgabe bleibt offen, nichts geht weiter.
190 if (e.reason !== 'answer') {
191 await update($, activeId, () => null)
192
193 return next(e)
194 }
195
196 // Aufgabe fertig: abhaken.
197 const now = await $.clock.now()
198 await setTasks($, list =>
199 list.map(t => (t.id === current ? { ...t, done: true, doneAt: now } : t)),
200 )
201 await update($, activeId, () => null)
202
203 // Nur mit Automatik an geht es zur naechsten Aufgabe weiter.
204 if (await read($, auto)) {
205 autoRuns += 1
206 if (autoRuns > AUTO_LIMIT) {
207 // Sicherheits-Stopp gegen Endlosschleifen.
208 await update($, auto, () => false)
209 await $.store.set('auto', false)
210 autoRuns = 0
211 $.ui.toast(
212 'Sicherheits-Stopp: ' + AUTO_LIMIT + ' Aufgaben am Stueck. Auto ist aus.',
213 )
214 } else {
215 const gestartet = await startNext($)
216 if (!gestartet) {
217 autoRuns = 0
218 $.ui.toast('Arbeitsliste abgearbeitet.')
219 }
220 }
221 }
222
223 return next(e)
224 })
225
226 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
227 const { Box, Text, Button, Input } = $.ui.resolve(e)
228 const list = await read($, tasks)
229 const active = await read($, activeId)
230 const isAuto = await read($, auto)
231 const editing = await read($, editingId)
232 const draftText = await read($, draft)
233
234 const open = list.filter(t => !t.done)
235 const done = list
236 .filter(t => t.done)
237 .sort((a, b) => (b.doneAt ?? 0) - (a.doneAt ?? 0))
238
239 // Claude arbeitet an der Liste, wenn gerade eine Aufgabe aktiv ist.
240 const busy = active !== null
241
242 // Platz fuer Kopf, Knopfzeile, Eingabe, offene Aufgaben und Rand abziehen.
243 const doneRoom = Math.max(1, (e.viewport?.rows ?? 24) - 12 - open.length)
244 const hidden = done.length - doneRoom
245
246 return (
247 <Box flexDirection="column" gap={1} paddingX={1} paddingY={1}>
248 <Box flexDirection="column">
249 {busy ? (
250 <Text dimColor>{'◆'} Claude arbeitet an der Liste</Text>
251 ) : (
252 <Text color={GREEN}>{'◆'} Claude ist frei</Text>
253 )}
254 <Text dimColor>
255 {isAuto
256 ? 'Automatik an: offene Aufgaben starten von allein.'
257 : 'Automatik aus: nichts laeuft von selbst. Mit ▶ startest du eine.'}
258 </Text>
259 </Box>
260
261 <Box gap={1}>
262 <Button
263 key="auto"
264 label={isAuto ? 'Auto: an' : 'Auto: aus'}
265 onPress={() => void toggleAuto($)}
266 />
267 <Button
268 key="clear-done"
269 label="Erledigte weg"
270 onPress={() => void setTasks($, l => l.filter(t => !t.done))}
271 />
272 </Box>
273
274 <Box flexDirection="column" gap={1}>
275 <Text bold>Deine Warteschlange - {open.length} offen</Text>
276 <Input
277 key="add"
278 placeholder="neue Aufgabe, Enter"
279 submitLabel="einreihen"
280 autoFocus
281 value={draftText}
282 onInput={value => void update($, draft, () => value)}
283 onSubmit={value => {
284 void addTask($, value)
285 void update($, draft, () => '')
286 }}
287 />
288 <Box flexDirection="column">
289 {open.length === 0 && <Text dimColor>Noch leer.</Text>}
290 {open.map(task => {
291 const isActive = task.id === active
292
293 if (task.id === editing) {
294 return (
295 <Box key={'row-' + task.id} gap={1} alignItems="flex-start">
296 <Input
297 key={'edit-' + task.id}
298 value={task.text}
299 submitLabel="speichern"
300 autoFocus
301 onSubmit={value => void saveEdit($, task.id, value)}
302 />
303 <Button
304 key={'cancel-' + task.id}
305 label="Abbrechen"
306 onPress={() => void update($, editingId, () => null)}
307 />
308 </Box>
309 )
310 }
311
312 return (
313 <Box key={'row-' + task.id} gap={1} alignItems="flex-start">
314 <Button
315 key={'chk-' + task.id}
316 label={'☐'}
317 onPress={() => void toggleDone($, task.id)}
318 />
319 <Button
320 key={'go-' + task.id}
321 label={'▶'}
322 onPress={() => void startTask($, task)}
323 />
324 <Button
325 key={'edit-btn-' + task.id}
326 label={'✎'}
327 onPress={() => void update($, editingId, () => task.id)}
328 />
329 <Button
330 key={'del-' + task.id}
331 label={'\u{1F5D1}'}
332 onPress={() => {
333 void setTasks($, l => l.filter(t => t.id !== task.id))
334 if (active === task.id) void update($, activeId, () => null)
335 if (editing === task.id)
336 void update($, editingId, () => null)
337 }}
338 />
339 <Text bold={isActive} color={isActive ? GREEN : undefined}>
340 {task.text}
341 </Text>
342 </Box>
343 )
344 })}
345 </Box>
346 </Box>
347
348 {done.length > 0 && (
349 <Box flexDirection="column" gap={1}>
350 <Text bold>Erledigt - {done.length}</Text>
351 <Box flexDirection="column">
352 {done.slice(0, doneRoom).map(task => (
353 <Box key={'done-' + task.id} gap={1} alignItems="flex-start">
354 <Button
355 key={'undone-' + task.id}
356 label={'☑'}
357 onPress={() => void toggleDone($, task.id)}
358 />
359 <Text dimColor strikethrough>
360 {task.text}
361 </Text>
362 </Box>
363 ))}
364 {hidden > 0 && <Text dimColor>... und {hidden} weitere</Text>}
365 </Box>
366 </Box>
367 )}
368 </Box>
369 )
370 })
371}
372types/index.d.ts 20 lines1export type Task = {
2 id: string
3 text: string
4 done: boolean
5 createdAt: number
6 doneAt: number | null
7}
8
9declare module 'claude-code' {
10 interface PluginState {
11 'todo-list': {
12 tasks: Task[]
13 activeId: string | null
14 auto: boolean
15 editingId: string | null
16 draft: string
17 }
18 }
19}
20