SLOPSHOPPER

sitzungs-waechter

Warnt mit einem Band ueber dem Prompt, wenn eine zweite Claude-Code-Sitzung im selben Ordner laeuft.

newbandtoasttimer
v0.1.0MITupdated 2026-10-12broening/claude-mods/sitzungs-waechter
A shopper browsing a rack in a slop shop
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
annahmenZeigt 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-boostKnopf 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-waechterWarnt ü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.–
grillStellt 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.

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 163 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3import type { Other } from '../types'
4
5// Sitzungs-Waechter: warnt, wenn eine zweite Claude-Code-Sitzung im selben
6// Ordner laeuft. Zwei Sitzungen, die dieselben Dateien aendern, ueberschreiben
7// sich sonst gegenseitig.
8//
9// Jede Sitzung traegt sich alle 30 Sekunden in eine gemeinsame Liste ein
10// (~/.claude/sitzungs-waechter.json): Sitzungs-ID, Ordner, Zeit. Eintraege
11// ohne Lebenszeichen seit 90 Sekunden gelten als tot und fliegen raus. Steht
12// eine andere lebende Sitzung mit demselben Ordner in der Liste, zeigt das Band
13// ueber dem Prompt eine Warnung. "ok" blendet sie aus, bis sich die Liste der
14// anderen Sitzungen aendert.
15
16const RED = '#ef4444'
17const BEAT_MS = 30_000
18const STALE_MS = 90_000
19const FILE_NAME = 'sitzungs-waechter.json'
20
21export type Entry = { dir: string; ts: number; start: number }
22export type Registry = { sessions: Record<string, Entry> }
23
24const others = atom({ plugin: 'sitzungs-waechter', key: 'others' } as const, [] as Other[])
25const dismissed = atom({ plugin: 'sitzungs-waechter', key: 'dismissed' } as const, '')
26
27let timer: { cancel: () => void } | undefined
28let started = 0
29
30// Ordner vergleichbar machen: Windows ist gross/klein egal, / und \ gleich.
31export function normDir(path: string): string {
32  return path.replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase()
33}
34
35// Liste lesen. Kaputt oder fehlend = leer.
36export function parseRegistry(text: string | undefined): Registry {
37  try {
38    const raw = JSON.parse(text ?? '') as Registry
39    if (raw && typeof raw.sessions === 'object' && raw.sessions) return { sessions: { ...raw.sessions } }
40  } catch {}
41  return { sessions: {} }
42}
43
44// Tote Eintraege raus, eigenen Eintrag setzen (oder mit entry undefined loeschen).
45export function merge(reg: Registry, id: string, entry: Entry | undefined, now: number): Registry {
46  const sessions: Record<string, Entry> = {}
47  for (const [key, e] of Object.entries(reg.sessions)) {
48    if (key !== id && e && now - e.ts < STALE_MS) sessions[key] = e
49  }
50  if (entry) sessions[id] = entry
51
52  return { sessions }
53}
54
55// Andere lebende Sitzungen im selben Ordner, die aelteste zuerst.
56export function othersIn(reg: Registry, id: string, dir: string, now: number): Other[] {
57  return Object.entries(reg.sessions)
58    .filter(([key, e]) => key !== id && e.dir === dir && now - e.ts < STALE_MS)
59    .map(([key, e]) => ({ id: key, start: e.start }))
60    .sort((a, b) => a.start - b.start)
61}
62
63function keyOf(list: Other[]): string {
64  return list.map(o => o.id).sort().join(',')
65}
66
67async function registryPath($: EngineInterface): Promise<string> {
68  const home = (await $.env.get('USERPROFILE')) ?? (await $.env.get('HOME')) ?? '.'
69  return home.replace(/[\\/]+$/, '') + '/.claude/' + FILE_NAME
70}
71
72async function load($: EngineInterface, path: string): Promise<Registry> {
73  const text = await $.fs.read(path).catch(() => undefined)
74  return parseRegistry(typeof text === 'string' ? text : undefined)
75}
76
77// Ein Lebenszeichen: eintragen, schreiben, andere Sitzungen pruefen.
78async function beat($: EngineInterface) {
79  try {
80    const path = await registryPath($)
81    const id = await $.session.id()
82    const dir = normDir(await $.session.root())
83    const now = await $.clock.now()
84    if (!started) started = now
85    const reg = merge(await load($, path), id, { dir, ts: now, start: started }, now)
86    await $.fs.write(path, JSON.stringify(reg, null, 1))
87
88    const list = othersIn(reg, id, dir, now)
89    const before = await read($, others)
90    await update($, others, () => list)
91    const fresh = list.some(o => !before.some(b => b.id === o.id))
92    if (fresh && keyOf(list) !== (await read($, dismissed))) {
93      $.ui.toast('Achtung: Eine zweite Sitzung arbeitet im selben Ordner.')
94    }
95  } catch {
96    // Waechter darf nie die Sitzung stoeren.
97  }
98}
99
100// Eigenen Eintrag loeschen (Sitzung endet oder /clear).
101async function leave($: EngineInterface, id: string) {
102  try {
103    const path = await registryPath($)
104    const now = await $.clock.now()
105    const reg = merge(await load($, path), id, undefined, now)
106    await $.fs.write(path, JSON.stringify(reg, null, 1))
107  } catch {}
108}
109
110export const register: Register = on => {
111  on('session.start', async ($, e, next) => {
112    timer?.cancel()
113    started = 0
114    await beat($)
115    timer = $.clock.every(BEAT_MS, () => void beat($))
116
117    return next(e)
118  })
119
120  on('session.end', async ($, e, next) => {
121    timer?.cancel()
122    timer = undefined
123    await leave($, e.sessionId)
124    // Nach /clear laeuft der Prozess weiter, aber ohne session.start.
125    if (e.reason === 'clear') {
126      started = 0
127      timer = $.clock.every(BEAT_MS, () => void beat($))
128    }
129
130    return next(e)
131  })
132
133  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
134    const below = await next(e)
135    if (e.props?.hasSurvey) return below
136
137    const list = await read($, others)
138    if (list.length === 0) return below
139    const key = keyOf(list)
140    if (key === (await read($, dismissed))) return below
141
142    const { Box, Text, Button } = $.ui.resolve(e)
143    const n = list.length
144    const label =
145      n === 1
146        ? 'Eine zweite Sitzung arbeitet im selben Ordner.'
147        : n + ' weitere Sitzungen arbeiten im selben Ordner.'
148
149    return (
150      <Box flexDirection="column">
151        {below}
152        <Box gap={1}>
153          <Text bold color={RED}>
154            {'⚠'}
155          </Text>
156          <Text>{label} Dateien koennen sich ueberschreiben.</Text>
157          <Button key="ok" label="ok" onPress={() => void update($, dismissed, () => key)} />
158        </Box>
159      </Box>
160    )
161  })
162}
163
types/index.d.ts 13 lines
1export type Other = { id: string; start: number }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'sitzungs-waechter': {
6      // Andere lebende Sitzungen im selben Ordner.
7      others: Other[]
8      // Schluessel der Liste, die der Nutzer mit "ok" weggeklickt hat.
9      dismissed: string
10    }
11  }
12}
13