SLOPSHOPPER

next-steps

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

newbandcommandtoastmodel
v0.1.1MITupdated 2026-10-06broening/claude-mods/next-steps
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · next-steps
› fix the failing auth test and add an audit log call ╭─────────────────╮ │ next-steps │ ⏺ Read(src/auth.ts) │ Vorschlaege aus │ ⎿ 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 › /vorschlaege ⎿ next-steps: Vorschlaege sind aus. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
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 185 lines
1import { 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}
185
types/index.d.ts 10 lines
1declare module 'claude-code' {
2  interface PluginState {
3    'next-steps': {
4      suggestions: string[]
5      loading: boolean
6      enabled: boolean
7    }
8  }
9}
10