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

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 163 lines1import { 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}
163types/index.d.ts 13 lines1export 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