Automatischer Push: 46 Datei(en) geändert

- .claude/agents/planer.md
- .claude/settings.json
- .claude/workflows/executer-welle.js
- .claude/workflows/plankritik.js
- .claude/workflows/planung.js
- .claude/workflows/quorum.js
- .claude/workflows/verbrauch.js
- .claude/workflows/welle-mit-pruefung.js
- .vault-tools/shell-wache/ENTWURF.md
- .vault-tools/shell-wache/docs/code/shell_wache.md
- .vault-tools/shell-wache/docs/plans/2026-10-30-shell-wache/00-uebersicht.md
- .vault-tools/shell-wache/docs/plans/2026-10-30-shell-wache/task-01-shell-wache.md
- … und 34 weitere

Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Pascal MichelandClaude Opus 5 committed 2026-10-31 03:00:01 +01:00
1 parent f2ccec39d6
commit 13213ea0ac
46 files changed
+2380 -48

No files matched your search

+77
View File
@@ -0,0 +1,77 @@
---
type: resource
title: "Entwurf: Shell-Wache"
status: active
summary: "Entscheidungsliste, Herkunft je Antwort, Angriffsliste und Abbruchbedingung für shell_wache.py, einen PreToolUse-Hook auf das Bash-Werkzeug, der bei vier bekannten Windows-Shell-Fallen einen Hinweis in den Kontext gibt und nie ablehnt."
tags: [vault-tools, pruefwerkzeuge]
created: 2026-10-30
updated: 2026-10-30
---
# Entwurf: Shell-Wache
Angelegt am 2026-10-30 als Entwurfsschritt nach [[AI/Regelwerk/pruefwerkzeuge|pruefwerkzeuge.md]], Abschnitt 1. Anlass ist Befund S-14 des Workflow-Reviews: Bekannte Shell-Fallen kehren trotz Erinnerung wieder. Pascal hat einen Hook beschlossen; das Prüferurteil hat ihn auf maschinell eindeutige Muster und auf Warnen begrenzt. Die Regeln selbst stehen in [[AI/Regelwerk/windows-shell|windows-shell.md]].
Plan: [00-uebersicht.md](docs/plans/2026-10-30-shell-wache/00-uebersicht.md) · Codedoku: [shell_wache.md](docs/code/shell_wache.md)
## Entscheidungsliste
| # | Frage | Antwort | Herkunft |
|---|---|---|---|
| 1 | Welches Ereignis, welcher Matcher? | `PreToolUse`, Matcher `Bash` | **Projektvorgabe** — Pascal, Workflow-Review 2026-10-30, S-14 |
| 2 | Lehnt der Hook ab? | Nie. Kein `permissionDecision` in der Ausgabe: `allow` würde die Berechtigungsabfrage überspringen, `deny` und `ask` halten den Befehl an | **Projektvorgabe** — Prüferurteil S-14 („nur warnen“) |
| 3 | Welche Ausgabeform? | Exit 0. Mit Befund genau ein JSON-Objekt `{"hookSpecificOutput": {"hookEventName": "PreToolUse", "additionalContext": "…"}}`, ohne Befund keine Ausgabe | **Quelle** — Schema im Programmabbild von Claude Code 2.1.309: `hookEventName:C("PreToolUse"), permissionDecision…optional(), …, additionalContext:o().optional()`, gelesen 2026-10-30 mit `grep -a` |
| 4 | Welche Exit-Codes? | Immer 0, auch bei kaputter Eingabe und internem Fehler | **Quelle** — Hilfetext im selben Abbild: „Exit code 2 - show stderr to model and block tool call“; „Other exit codes - show stderr to user only“. Exit 2 wäre eine Ablehnung, andere Codes erreichten das Modell nicht |
| 5 | Woher kommt der Befehl? | `tool_input.command` der Hook-Eingabe auf stdin | **Quelle** — Hilfetext: „Input to command is JSON of tool call arguments“; das Feld `command` ist das Argument des Bash-Werkzeugs. Erst die Probe-Sitzung belegt die Form am echten Aufruf |
| 6 | Welche Muster? | Genau vier: `python -I` ohne `-X utf8`, `pkill` mit `-f`, `python3`, `bash` ohne Pfad im Windows-Kontext | **Projektvorgabe** — Auftrag nach S-14; Fallgeschichten in den Erinnerungen `python-isoliert-ignoriert-pythonioencoding`, `pkill-f-trifft-die-eigene-shell`, `bash-startet-unter-windows-den-wsl-stub` |
| 7 | Werden Heredoc-Backslashes geprüft? | Nein | **Projektvorgabe** — Prüferurteil S-14: Die Verfälschung passiert in der Werkzeugschicht und ist im Befehlstext nicht als Fehler erkennbar |
| 8 | Was ist „Windows-Kontext“ für `bash`? | (a) der Rest der Zeile hinter `cmd /c`, `cmd //c`, `cmd.exe /c`, `powershell`, `pwsh` (mit oder ohne `.exe`, mit oder ohne Pfad); (b) im ganzen Befehl ein Python-Listenelement `["bash"` oder `['bash.exe'` | **Projektvorgabe** — gemessen 2026-10-30 im Bash-Werkzeug: `type -a bash` liefert zuerst `/usr/bin/bash`; ein `bash` auf oberster Ebene der Git-Bash ist also die Git-Bash und bleibt still. Windows selbst sucht zuerst in `System32` (windows-shell.md) |
| 9 | Wann gilt `bash` als „mit Pfad“? | Steht direkt davor `/` oder `\`, bleibt es still | **Projektvorgabe** |
| 10 | Was ist eine Linux-Weiche? | Ein Wort `wsl`, `ssh`, `docker`, `podman` oder `kubectl` (mit oder ohne `.exe`). Im Windows-Kontext (a) bleibt ein `bash` hinter einer Weiche auf derselben Zeile still. `python3` und Muster (b) bleiben im ganzen Befehl still, sobald irgendwo eine Weiche steht | **Projektvorgabe** — dort läuft der Befehl unter Linux, wo `python3` und `bash` richtig sind |
| 11 | Wann ist `python -I` ohne `-X utf8`? | Je Aufruf von `python` oder `python.exe` (mit oder ohne Pfad): Optionen bis zum ersten Nicht-Optionswort lesen; `-c` und `-m` beenden die Optionen, auch in einem Bündel wie `-Ic`; `-W` und `-X` nehmen ein Argument, angehängt oder als nächstes Wort. Treffer, wenn `I` in einem Bündel steht und kein `-X utf8` oder `-X utf8=1` davor | **Quelle** — Python-Kommandozeile (`python --help`): `-I` schließt `-E` ein, `-c` und `-m` beenden die Optionsliste. **Projektvorgabe:** `-X utf8=0` zählt nicht als utf8 |
| 12 | Gilt das `-I`-Muster auch für `python3`? | Nein, dafür gibt es Muster `python3` | **Projektvorgabe** — ein Hinweis je Fehler |
| 13 | Wann ist `pkill` mit `-f`? | Hinter dem Wort `pkill` (mit oder ohne Pfad) die Optionswörter bis zum ersten Nicht-Optionswort: Treffer bei `--full` oder einem Bündel aus Buchstaben mit kleinem `f` (`-f`, `-fx`). `-u`, `-U`, `-g`, `-G`, `-P`, `-s`, `-t`, `-F`, `--signal` nehmen das nächste Wort als Argument. Auch in WSL und über ssh | **Quelle** — `pkill(1)`, procps-ng: `-f, --full`. **Projektvorgabe:** keine Weiche, weil die Falle gerade unter Linux zuschlägt; `pgrep -f` bleibt still, es beendet nichts |
| 14 | Was meldet der Hook bei kaputter Eingabe? | Einen Hinweis `[eingabe]` mit dem Grund (leer, kein JSON, kein Objekt, `tool_input` oder `command` fehlt oder hat den falschen Typ) | **Projektvorgabe** — pruefwerkzeuge.md, Abschnitt 6: eine fehlende Voraussetzung ist ein Befund |
| 15 | Was bei leerem `command`? | Still | **Projektvorgabe** — nichts zu prüfen, Abschnitt 6 |
| 16 | Was bei `tool_name` ungleich `Bash`? | Hinweis `[eingabe]`, nichts geprüft. Fehlt `tool_name`, wird trotzdem geprüft | **Projektvorgabe** — ein erweiterter Matcher soll laut auffallen, nicht mit falscher Kontextregel prüfen |
| 17 | Was bei einem internen Fehler? | Gefangen, Hinweis `[intern]` mit Ausnahmetyp, die bis dahin gesammelten Befunde stehen trotzdem im Text | **Projektvorgabe** — pruefwerkzeuge.md, Abschnitt 4 |
| 18 | Wie sieht der Hinweis aus? | Erste Zeile: „shell-wache warnt nur, der Befehl läuft trotzdem.“ Danach je getroffenem Muster genau eine Zeile mit Schlüssel in eckigen Klammern, Abhilfe und Verweis auf `AI/Regelwerk/windows-shell.md`, bei `pkill` zusätzlich auf die Erinnerung | **Projektvorgabe** |
| 19 | Groß- und Kleinschreibung? | `cmd`, `powershell`, `pwsh`, `bash`, `python` ohne Rücksicht darauf; `pkill` und seine Optionen genau | **Projektvorgabe** — Windows-Namen sind beliebig geschrieben, Linux-Optionen nicht |
| 20 | Wie liest das Skript stdin? | Als Bytes, Dekodierung UTF-8 mit Ersatzzeichen; Aufruf `python -I -X utf8` | **Projektvorgabe** — windows-shell.md, „Welche Shell, welches Python“ |
## Angriffsliste
Je Fall ein Treffer und ein gesunder Gegenfall; jeder Treffer einmal am Anfang und einmal am Ende des Befehls.
| Klasse | Fälle |
|---|---|
| fehlt | stdin leer; `tool_input` fehlt; `command` fehlt |
| null | `null` als ganze Eingabe, als `tool_input`, als `command` |
| falscher Typ | Liste statt Objekt; `tool_input` als Zeichenkette; `command` als Zahl und als Liste |
| leer | `command` gleich `""` (still) |
| zusätzlich | unbekannte Felder in Eingabe und `tool_input` (still); `tool_name` gleich `PowerShell` (Hinweis `[eingabe]`) |
| doppelt | dasselbe Muster zweimal in einem Befehl: eine Zeile |
| mehrere | alle vier Muster in einem Befehl: vier Zeilen |
| Wertebereich | `-IB`, `-BI`, `-Ic`, `-IX utf8`, `-Xutf8`, `-X utf8=0`, `-I` hinter `-c` oder hinter dem Skriptnamen; `pkill -TERM -f`, `pkill -fx`, `pkill --full`, `pkill -u root -f`; `python3.12`, `python3-config`; `bash.exe`, `.bashrc`, `bash_profile`, `bash-completion` |
| Reihenfolge | jedes Muster am Anfang und am Ende, dazu in Zeile 3 eines mehrzeiligen Befehls |
| Umgebung | ungültiges UTF-8 auf stdin; kein JSON; Aufruf ohne `PYTHONIOENCODING` |
## Abbruchbedingung
Schluss ist, wenn eine Prüfrunde nur noch Umformulierungen oder neue Fehlalarm-Grenzfälle in Zeichenketten findet. Fehlalarme kosten hier eine Zeile Kontext, keinen abgelehnten Befehl; sie werden als Lücke benannt, nicht wegprogrammiert.
## Benannte Lücken
- Erwähnungen in Zeichenketten schlagen an, etwa `grep "python -I" datei` oder eine Commit-Nachricht. Der Hook kennt keine Shell-Grammatik.
- Das PowerShell-Werkzeug ist nicht abgedeckt; dort ist jeder Befehl Windows-Kontext. Eine Erweiterung bräuchte eine eigene Kontextregel.
- `py` (Python-Launcher), `subprocess` mit `shell=True` und Skriptdateien, die erst zur Laufzeit gelesen werden, sieht der Hook nicht.
- Ein Befehl, der ein lokales `python3` und zugleich `ssh` enthält, bleibt still (Weiche gilt für den ganzen Befehl).
- `cmd` zählt nur mit `/c` oder `//c` direkt hinter dem Namen; `cmd /d /c` oder `cmd /s /c` sind kein Windows-Kontext.
- Im Windows-Kontext zählt nur die erste Weiche einer Zeile: `powershell -c "wsl -- bash a; bash b"` bleibt still.
- Workflow-Subagenten laufen mit derselben `settings.json`; der Hook wirkt dort mit. Ob ihr Kontext den Hinweis zeigt, belegt erst ein Lauf.
- Dieser Hook ist ein Fangnetz, kein Nachweis. Er erkennt Muster im Befehlstext, nicht, was der Befehl tut.
## Referenzen
- [[AI/Regelwerk/pruefwerkzeuge|↑ Prüfwerkzeuge]]
- [[AI/Regelwerk/windows-shell|windows-shell.md]]
@@ -0,0 +1,50 @@
---
type: resource
title: "shell_wache.py"
status: active
summary: "Codedoku zum PreToolUse-Hook, der im Befehl des Bash-Werkzeugs vier Windows-Shell-Fallen erkennt und einen Hinweis in den Kontext gibt, ohne je abzulehnen."
tags: [vault-tools, codedoku]
created: 2026-10-30
updated: 2026-10-30
---
Werkzeug: [ENTWURF.md](../../ENTWURF.md)
# shell_wache.py
## Wofür
- Warnt vor `python -I` ohne `-X utf8`, `python3`, `bash` ohne Pfad aus cmd, PowerShell oder Python und `pkill -f`, bevor das Bash-Werkzeug den Befehl ausführt.
## Ablauf
1. Claude Code startet das Skript vor jedem Aufruf des Bash-Werkzeugs, eingetragen in `.claude/settings.json` unter `PreToolUse`, Matcher `Bash`, als `python -I -X utf8`, Zeitlimit 10 s.
2. `lies_eingabe()` liest stdin als Bytes und holt `tool_input.command`. Kaputte Eingabe ergibt den Grund für `[eingabe]`, ein leerer Befehl bleibt still.
3. Die vier Detektoren in `DETEKTOREN` laufen je in eigenem `try`. Ein Absturz darin wird `[intern]`, die übrigen laufen weiter.
4. `baue_text()` setzt Kopfzeile und je getroffenem Schlüssel eine Zeile aus `HINWEISE` zusammen.
5. `melde()` schreibt genau ein JSON-Objekt mit `hookSpecificOutput.additionalContext`. Ohne Befund schreibt das Skript nichts.
6. Das Skript endet immer mit Exit 0.
```bash
# Regressionstreiber: Grundlinie, dann alle Fälle, je Fall ein eigener Prozess
python -I -X utf8 .vault-tools/shell-wache/faelle_shell_wache.py
# Gegen eine mutierte Kopie: Pfad zum Hook als erstes Argument
python -I -X utf8 .vault-tools/shell-wache/faelle_shell_wache.py /pfad/zur/kopie/shell_wache.py
```
## Annahmen
- Claude Code liefert die Hook-Eingabe als JSON mit `tool_name` und `tool_input.command`. Belegt am 2026-10-30 in einer frischen Sitzung mit Claude Code 2.1.309: Im Transkript stehen `hook_success` und `hook_additional_context` für `PreToolUse:Bash`.
- `additionalContext` bei PreToolUse erreicht das Modell. Belegt in derselben Probe: Das Modell gab den Hinweis wörtlich wieder.
- Das Bash-Werkzeug ist die Git-Bash. Ein `bash` auf oberster Ebene ist dort `/usr/bin/bash` (`type -a bash`, 2026-10-30) und bleibt still.
- Eine geänderte `settings.json` wirkt in laufenden Sitzungen sofort. Am 2026-10-30 schlug der Hook in der Sitzung, die ihn eingetragen hat, beim nächsten Bash-Aufruf an.
## Was beim Anfassen weh tut
- **Nie Exit 2 und nie `permissionDecision`.** Exit 2 lehnt den Befehl ab, `allow` überspringt die Berechtigungsabfrage. Beides fängt der Treiber, die letzte Schranke in `__main__` hält Exit 0 auch bei Absturz.
- **Keine Importe aus dem eigenen Ordner.** `python -I` nimmt den Skriptordner aus `sys.path`.
- **Erwähnungen schlagen an.** Ein Befehl, der `pkill -f` nur als Text enthält, löst den Hinweis aus. Das ist eine benannte Lücke, kein Fehler. Wer sie schließen will, braucht eine Shell-Grammatik, keinen weiteren regulären Ausdruck.
- **Die Linux-Weiche gilt für den ganzen Befehl.** Wer sie auf Zeilen einschränkt, macht `ssh h python - <<'EOF'` mit `["bash"` im Heredoc laut.
- **Zwei Mutanten sind wirkungslos:** `-c` nicht als Ende der Python-Optionen zu werten und einen leeren Befehl an die Detektoren zu geben. In beiden Fällen ändert sich keine Ausgabe, deshalb fängt sie kein Fall.
- **Ein neues Muster braucht einen Treffer- und einen Gegenfall im Treiber, je am Anfang und am Ende des Befehls,** und einen Rotlauf mit gelöschtem Detektor.
- **Probe-Eingaben mit Backslash legst du mit dem Write-Werkzeug an.** Ein `printf` mit `\\` in einem Bash-Aufruf kam am 2026-10-30 als einzelnes `\` an, und der Hook meldete richtig „kein JSON“.
@@ -0,0 +1,24 @@
---
type: plan
title: "Shell-Wache und Haiku für Werkzeug-Hilfsagenten"
status: active
summary: "Zwei beschlossene Änderungen aus dem Workflow-Review 2026-10-30: ein warnender PreToolUse-Hook gegen vier Windows-Shell-Fallen (S-14) und Haiku 5.5 für Hilfsagenten der Workflows, deren Ausgabe nur das Skript liest (S-17)."
tags: [plan, vault-tools]
created: 2026-10-30
updated: 2026-10-30
---
Entwurf: [ENTWURF.md](../../../ENTWURF.md)
# Shell-Wache und Haiku für Werkzeug-Hilfsagenten
Anlass: Workflow-Review vom 2026-10-30, Befunde S-14 und S-17 mit Prüferurteil. Beide Teile berühren getrennte Dateien und laufen als eine Welle.
| Task | Thema | Zieldateien | Modell |
|---|---|---|---|
| [01](task-01-shell-wache.md) | Hook und Regressionstreiber | `.vault-tools/shell-wache/shell_wache.py`, `.vault-tools/shell-wache/faelle_shell_wache.py` | sonnet |
| [02](task-02-hilfsagenten-haiku.md) | Haiku für Hilfsagenten, Kommentare berichtigen, Fälle | `.claude/workflows/quorum.js`, `plankritik.js`, `welle-mit-pruefung.js`, `executer-welle.js`, `.vault-tools/workflow-pruefung/faelle_workflows.mjs`, `.vault-tools/workflow-pruefung/mutanten_workflows.py` | sonnet |
Nach der Welle macht der Koordinator selbst: Gegenprüfung, Mutanten am Hook, Eintrag in `.claude/settings.json`, Probe-Sitzung, echte Workflow-Läufe, Codedoku.
Nicht in dieser Welle: die Zeile „Werkzeug-Hilfsagent“ in der Rollentabelle von `CLAUDE.md` und der Hinweis in `AI/Regelwerk/navigation.md`. Beide Dateien bearbeitet zur selben Zeit eine andere Sitzung.
@@ -0,0 +1,115 @@
---
type: plan
title: "Task 01: shell_wache.py und Regressionstreiber"
status: active
modell: sonnet
summary: "PreToolUse-Hook, der im Befehl des Bash-Werkzeugs vier Windows-Shell-Fallen erkennt und nur warnt, dazu ein Treiber mit Treffer- und Gegenfall je Muster."
tags: [plan, vault-tools]
created: 2026-10-30
updated: 2026-10-30
---
Übersicht: [00-uebersicht.md](00-uebersicht.md)
# Task 01: shell_wache.py und Regressionstreiber
## Ziel
Zwei neue Dateien, sonst nichts:
- `D:/Claude/.vault-tools/shell-wache/shell_wache.py`
- `D:/Claude/.vault-tools/shell-wache/faelle_shell_wache.py`
**Verboten:** `.claude/settings.json` anfassen, irgendeine andere Datei ändern, committen. Nur Standardbibliothek, keine Importe aus dem eigenen Ordner (der Hook läuft mit `python -I`, das den Skriptordner aus `sys.path` nimmt). Python immer als `python -I -X utf8`, nie `python3`. Kein Heredoc für Python-Code: Dateien mit dem Write-Werkzeug anlegen. Vor jedem Lauf `python -m py_compile <datei>`.
Die verbindliche Spezifikation ist die Entscheidungsliste in `D:/Claude/.vault-tools/shell-wache/ENTWURF.md` (Fragen 1 bis 20) samt Angriffsliste. Lies sie ganz. Was hier steht, ergänzt sie; bei Widerspruch gilt ENTWURF.md, und du meldest den Widerspruch.
## shell_wache.py
**Kopfkommentar** (Prüfskript, also volle Länge nach `AI/Regelwerk/codedoku.md`, „Ausnahme: Prüfskripte“), im Stil von `D:/Claude/.vault-tools/session-hooks/regelwerk_nach_edit.py`:
```
"""PreToolUse-Hook auf das Bash-Werkzeug: warnt vor vier bekannten Windows-Shell-Fallen, lehnt nie ab.
Ersteller: Pascal Michel
Erstellt: 2026-10-30
Entwurf: ENTWURF.md im selben Ordner · Doku: docs/code/shell_wache.md
GELTUNGSBEREICH
(vier Muster, Windows-Kontext, Linux-Weiche, Ausgabeform, immer Exit 0 – knapp aus ENTWURF.md)
BENANNTE LÜCKEN
(die Liste aus ENTWURF.md, Abschnitt „Benannte Lücken“)
DIESER HOOK IST EIN FANGNETZ, KEIN NACHWEIS. ...
"""
```
**Ablauf:**
1. stdin als Bytes lesen, `decode("utf-8", "replace")`.
2. Eingabe prüfen (ENTWURF 14 bis 16). Gründe für `[eingabe]`, wörtlich: `stdin leer`, `kein JSON`, `kein JSON-Objekt`, `tool_input fehlt oder ist kein Objekt`, `command fehlt oder ist keine Zeichenkette`, `tool_name ist <name>, nicht Bash`. Leerer `command` (`""` oder nur Leerraum): still.
3. Die vier Detektoren laufen je in eigenem `try`; ein Fehler darin wird `[intern]` mit `type(e).__name__`, die übrigen laufen weiter.
4. Ausgabe: ohne Befund nichts auf stdout. Mit Befund `json.dumps({"hookSpecificOutput": {"hookEventName": "PreToolUse", "additionalContext": text}}, ensure_ascii=True)` auf stdout. Nie `permissionDecision`, nie `decision`, nie `continue`.
5. `main()` in `try/except BaseException` (außer `KeyboardInterrupt` ist egal); auch ein Fehler dort wird als `[intern]` ausgegeben. **Das Skript endet in jedem Fall mit Exit 0** – nach der Ausgabe `sys.exit(0)` am Ende von `if __name__ == "__main__":`.
**Text des Hinweises:** erste Zeile wörtlich `shell-wache warnt nur, der Befehl läuft trotzdem.` Danach je getroffenem Schlüssel genau eine Zeile in dieser Reihenfolge, auch wenn das Muster mehrfach trifft:
| Schlüssel | Zeile (wörtlich) |
|---|---|
| `[python-I]` | `[python-I] python -I ohne -X utf8: -I ignoriert PYTHONIOENCODING, Umlaute brechen unter cp1252. Nimm python -I -X utf8 (AI/Regelwerk/windows-shell.md, „Welches Python“).` |
| `[python3]` | `[python3] python3 ist unter Windows der Microsoft-Store-Stub. Nimm python (AI/Regelwerk/windows-shell.md, „Welches Python“).` |
| `[bash-windows]` | `[bash-windows] bash ohne Pfad aus cmd, PowerShell oder Python startet unter Windows den WSL-Starter. Git-Bash mit vollem Pfad C:\Program Files\Git\bin\bash.exe, WSL mit wsl -d <Distro> (AI/Regelwerk/windows-shell.md, „Welches Python“).` |
| `[pkill-f]` | `[pkill-f] pkill -f trifft jede Befehlszeile mit dem Muster, auch die eigene Shell und fremde Läufe in WSL. Erst pgrep -af lesen, dann gezielt kill <pid> oder ein Muster wie "[s]leep 7199" (Erinnerung pkill-f-trifft-die-eigene-shell; AI/Regelwerk/windows-shell.md).` |
| `[eingabe]` | `[eingabe] Hook-Eingabe unbrauchbar (<grund>), nichts geprüft.` |
| `[intern]` | `[intern] shell-wache ist abgestürzt (<Ausnahmetyp>), Prüfung unvollständig.` |
## Detektoren
Regeln genau nach ENTWURF.md, Fragen 8 bis 13 und 19. Ergänzend, damit es eindeutig ist:
- **Wortgrenze:** Ein Programmwort beginnt am Befehlsanfang oder nach Leerraum, `;`, `&`, `|`, `(`, `` ` ``, `'`, `"`, `=`; es darf einen Pfad davor tragen (`/c/x/python.exe`, `"C:/P/python.exe"`), außer bei `bash` (Frage 9). Es endet vor Leerraum, `'`, `"`, `;`, `)`, `|`, `&` oder dem Textende. `python3-config`, `.bashrc`, `bash_profile`, `bash-completion`, `ipython`, `mypython` sind keine Programmwörter.
- **`python3`:** `python3` und `python3.<n>`, mit oder ohne `.exe`.
- **Optionen lesen** (`python`, `pkill`): den Rest der Zeile nach dem Programmwort an Leerraum zerlegen, je Wort umschließende `'` und `"` abstreifen, Wörter lesen, solange sie mit `-` beginnen und nicht genau `-` oder `--` sind.
- **Windows-Kontext (a):** gilt vom Kontextwort bis zum Zeilenende. Darin trifft jedes `bash` oder `bash.exe` ohne Pfad, vor dem auf derselben Zeile hinter dem Kontextwort keine Weiche steht.
- **Muster (b):** regulärer Ausdruck auf `[` plus optionalem Leerraum plus `"bash"`, `'bash'`, `"bash.exe"` oder `'bash.exe'`, ohne Rücksicht auf Groß- und Kleinschreibung.
## faelle_shell_wache.py
Treiber im Stil von `D:/Claude/.vault-tools/session-hooks/faelle_nach_edit.py`: startet den Hook je Fall als eigenen Prozess (`[sys.executable, "-I", "-X", "utf8", HOOK]`, stdin als Bytes, Zeitlimit 10 s), ohne `PYTHONIOENCODING` in der Umgebung. Je Fall prüft er:
- Exit-Code ist 0 (nie 2),
- stdout ist leer, wenn der Fall still sein soll; sonst genau ein JSON-Objekt, `hookSpecificOutput.hookEventName == "PreToolUse"`, kein Schlüssel `permissionDecision` irgendwo, und die Menge der Schlüssel in eckigen Klammern am Zeilenanfang von `additionalContext` ist genau die erwartete,
- die Laufzeit liegt unter 2 s.
**Grundlinie zuerst:** Ein gesunder Befehl (`ls -la && python -I -X utf8 x.py`) muss still sein. Scheitert sie, bricht der Treiber ab, weil der Rest nichts misst.
**Fälle**, jeder als Eintrag `(name, stdin_bytes, erwartete_schluessel)`. Ein Hilfsaufbau `eingabe(cmd)` baut `{"tool_name": "Bash", "tool_input": {"command": cmd}}`. Pflicht, mindestens:
| Gruppe | Treffer | Gegenfall (still) |
|---|---|---|
| python-I | `python -I x.py`; `python -IB x.py`; `python -BI x.py`; `python -Ic "print(1)"`; `python -I -c "x" -X utf8`; `python -I -X utf8=0 x.py`; `/c/Py/python.exe -I x.py`; `"C:/Py/python.exe" -I x.py`; `cd a && python -I x.py`; `PYTHONIOENCODING=utf-8 python -I x.py` | `python -I -X utf8 x.py`; `python -IXutf8 x.py`; `python -IX utf8 x.py`; `python -X utf8 -I -B x.py`; `python -I -X utf8=1 x.py`; `python x.py -I`; `python -c "import x" -I`; `ipython -I`; `python -m pytest -I` |
| python3 | `python3 x.py`; `python3.12 -m venv v`; `ls; python3 -V`; `python3.exe x.py` | `python x.py`; `python3-config --libs`; `wsl -d Debian -- python3 x.py`; `ssh h python3 x.py`; `docker exec c python3 x.py` |
| bash-windows | `cmd //c "bash x.sh"`; `cmd.exe /c bash -c "x"`; `CMD /C BASH x`; `powershell -c "bash -lc 'x'"`; `powershell.exe -NoProfile -Command "& bash x"`; `pwsh -c "bash x"`; `python -X utf8 -c "import subprocess; subprocess.run(['bash', '-c', 'x'])"`; `python x.py <<'EOF'` + Zeilenumbruch + `subprocess.run(["bash.exe", "x"])` + Zeilenumbruch + `EOF` | `bash -c "x"`; `bash x.sh`; `cmd //c "C:/Program Files/Git/bin/bash.exe" x`; `cmd //c "C:\Program Files\Git\bin\bash.exe" x`; `powershell -c "wsl -d Debian -- bash -lc 'x'"`; `cmd /c "ssh.exe -o A=B root@h bash -s < f.sh"`; `powershell -c "Get-Content ~/.bashrc"`; `powershell -c "x"` + Zeilenumbruch + `bash y.sh`; `ssh h python - <<'EOF'` + Zeilenumbruch + `subprocess.run(['bash','x'])` + Zeilenumbruch + `EOF`; `subprocess.run([GITBASH, '-c'])` in `python -c` |
| pkill-f | `pkill -f x`; `pkill -TERM -f "[s]leep 1"`; `pkill -fx x`; `pkill --full x`; `pkill -u root -f x`; `sudo pkill -f x`; `wsl -d Debian -- bash -c 'a; pkill -f "y"'`; `/usr/bin/pkill -f x` | `pkill x`; `pkill -TERM x`; `pgrep -f x`; `pkill -u fx x` (fx ist Argument von -u) |
| mehrere | alle vier Muster in einem Befehl: vier Schlüssel; `python -I a.py; python -I b.py`: ein Schlüssel | |
| Reihenfolge | je Muster: Treffer am Anfang (`<treffer>; ls`), am Ende (`ls; <treffer>`), in Zeile 3 eines dreizeiligen Befehls | |
| Eingabe | leer; `nicht json`; `[]`; `null`; `{}`; `{"tool_name": "Bash"}`; `tool_input` `null`, als Zeichenkette `"x"`; `command` fehlt, `null`, `5`, `["ls"]`; ungültiges UTF-8 (`b'{"tool_name":"Bash","tool_input":{"command":"ls \xff"}}'`, still); `tool_name` `PowerShell` mit `python3 x` → nur `[eingabe]` | `command` gleich `""` und `" "`: still; zusätzliche Felder (`"tool_use_id"`, `"description"`, `"timeout"`): still |
Ausgabe des Treibers: je Fall eine Zeile `OK` oder `FEHLER` mit Name und Abweichung, am Ende die Zahl der Fälle und der Fehler, gerechnet, nicht geschrieben. Exit 0 nur, wenn alle Fälle passen; Exit 1 sonst; ein Absturz des Treibers selbst wird gefangen und als Fehler gezählt.
## Prüfen
```bash
# Syntax beider Dateien
python -I -X utf8 -m py_compile D:/Claude/.vault-tools/shell-wache/shell_wache.py D:/Claude/.vault-tools/shell-wache/faelle_shell_wache.py
# Alle Fälle
python -I -X utf8 D:/Claude/.vault-tools/shell-wache/faelle_shell_wache.py
# Kein CR in den neuen Dateien (je 0)
tr -cd '\r' < D:/Claude/.vault-tools/shell-wache/shell_wache.py | wc -c
tr -cd '\r' < D:/Claude/.vault-tools/shell-wache/faelle_shell_wache.py | wc -c
```
**Rotnachweis, Pflicht im Bericht:** Kopiere `shell_wache.py` in einen neuen Ordner unter dem Scratchpad oder `mktemp -d`, lösche dort je Detektor dessen Aufruf (vier Mutanten, einzeln, mit Rücksetzen) und fahre den Treiber gegen die Kopie (der Treiber nimmt den Hook-Pfad als optionales erstes Argument). Jeder Mutant muss mindestens einen `FEHLER` erzeugen. Nenne je Mutant die Zahl der Fehler.
Berichte: Zahl der Fälle, Ergebnis, die vier Mutanten mit Fehlerzahl, jede Stelle, an der du von diesem Task oder ENTWURF.md abgewichen bist, und jeden Fall, den du nicht eindeutig umsetzen konntest.
@@ -0,0 +1,115 @@
---
type: plan
title: "Task 02: Haiku für Werkzeug-Hilfsagenten, deren Ausgabe nur das Skript liest"
status: active
modell: sonnet
summary: "In quorum.js, plankritik.js und welle-mit-pruefung.js laufen Hilfsagenten, deren Ausgabe nur das Skript liest, auf haiku; Hilfsagenten, deren Rohausgabe an Opus geht, bleiben auf sonnet. Veraltete Haiku-Kommentare in quorum.js und executer-welle.js berichtigen, Fälle und Mutanten nachziehen."
tags: [plan, vault-tools]
created: 2026-10-30
updated: 2026-10-30
---
Übersicht: [00-uebersicht.md](00-uebersicht.md)
# Task 02: Haiku für Werkzeug-Hilfsagenten
## Ziel und Grenzen
Zieldateien, sonst keine:
- `D:/Claude/.claude/workflows/quorum.js`
- `D:/Claude/.claude/workflows/plankritik.js`
- `D:/Claude/.claude/workflows/welle-mit-pruefung.js`
- `D:/Claude/.claude/workflows/executer-welle.js`
- `D:/Claude/.vault-tools/workflow-pruefung/faelle_workflows.mjs`
- `D:/Claude/.vault-tools/workflow-pruefung/mutanten_workflows.py`
**Verboten:** jede andere Datei, besonders `CLAUDE.md` und `AI/Regelwerk/*` (eine andere Sitzung bearbeitet sie gerade); committen; einen Workflow über das Workflow-Werkzeug starten; `sed` oder globales Ersetzen – nur das Edit-Werkzeug mit eindeutigen Stellen. Die `.js`-Dateien tragen LF und müssen es behalten. Kein Verhalten außer dem Modell ändern.
Regel, nach der entschieden ist (Prüferurteil S-17, Workflow-Review 2026-10-30): `model: 'haiku'` nur für Hilfsagenten, deren Ausgabe ausschließlich das Skript liest. Geht die Rohausgabe wörtlich an einen Opus-Agenten (Linse, pruefer), bleibt `sonnet`. `effort: 'low'` bleibt überall.
| Datei | Label | Modell neu | Grund |
|---|---|---|---|
| quorum.js | `werkzeug:paket` | haiku | `lesePaket()` liest nur `QUORUM_PAKET` und `EXIT` |
| quorum.js | `codex:start`, `codex:warten:<n>`, `codex:abbruch` | haiku | das Skript liest nur `EXIT`-Zeilen, `jobId`, `waitTimedOut` |
| quorum.js | `codex:ende` | haiku | Felder gehen an `codexUngueltig()` und als Befunde in den Bericht |
| quorum.js | `zusammenfuehren` | bleibt sonnet, medium | urteilt, überträgt nicht |
| plankritik.js | `werkzeug:kopie` | haiku | `kopieAusLauf()` liest nur `KOPIE=` und die Blockzählung |
| plankritik.js | `werkzeug:planwerkzeuge`, `werkzeug:mutanten` | bleibt sonnet | Rohausgabe geht wörtlich an die Linsen |
| welle-mit-pruefung.js | `werkzeug:kopie` | haiku | das Skript liest `KOPIE=` und die Blockzählung; Rückgabe an den Koordinator, nicht an den pruefer |
| welle-mit-pruefung.js | `werkzeug:vergleich` | bleibt sonnet | Rohausgabe geht als `schwerpunkt` an den pruefer |
## Änderungen im Code
1. **quorum.js:** In `werkzeugAgent` `model: 'sonnet'` → `model: 'haiku'` (alle Aufrufer stehen oben in der Tabelle auf haiku). Im `agent()`-Aufruf mit `label: 'codex:ende'` ebenso. `zusammenfuehren` und `nachpruefung` nicht anfassen.
2. **plankritik.js:** `werkzeugAgent(label, phase, befehl, zweck, zusatz)` bekommt einen sechsten Parameter `modell = 'sonnet'` und setzt `model: modell`. Nur der Aufruf `'werkzeug:kopie'` übergibt `'haiku'` (als `zusatz` dann `''`).
3. **welle-mit-pruefung.js:** `werkzeugAgent(label, phase, befehl, zweck)` bekommt einen fünften Parameter `modell = 'sonnet'` und setzt `model: modell`. Nur der Aufruf `'werkzeug:kopie'` übergibt `'haiku'`.
4. **executer-welle.js:** Verhalten bleibt. Nur Text:
- Zeile 12 `// Haiku kennt keinen Effort.` ersetzen durch `// Bei haiku verwirft effortFuer() einen Task-Effort; es gilt das medium aus dem Frontmatter, das Haiku 5.5 auch erhält (AI/Regelwerk/haiku-5-5.md, A1 und A7).`
- Logzeile in `effortFuer`: `` `${t.datei}: effort ${t.effort} bei haiku wirkungslos, nicht gesetzt` `` ersetzen durch `` `${t.datei}: effort ${t.effort} bei haiku nicht gesetzt, es gilt das medium aus dem Frontmatter des executer` ``.
## Änderungen an Kommentaren
- **quorum.js, Kopfkommentar „Paket:“** – `Hilfsagent (sonnet, low)` → `Hilfsagent (haiku, low)`.
- **quorum.js, Zeilen 35 und 36** – die zwei Zeilen ab `// - Die Codex-Hilfsagenten laufen auf sonnet mit low` ersetzen durch genau diese drei:
```
// - Die Hilfsagenten für Paket und Codex laufen auf haiku mit low: feste Befehle, das Skript liest nur
// Marken und Felder. Haiku 4.5 ersetzte typografische Zeichen still; Haiku 5.5 hielt sie in 6 von 6
// Läufen (AI/Regelwerk/haiku-5-5.md, A3). codex:ende überträgt Befundtexte, der Byte-Vergleich fehlt dort.
```
- **plankritik.js, Kopfkommentar** – hinter `// gleich treu wie high bei mechanischer Arbeit, codeworkflow.md "Effort je Rolle").` diese zwei Zeilen einfügen:
```
// Modell: werkzeug:kopie läuft auf haiku, weil nur kopieAusLauf() die Ausgabe liest. Planwerkzeuge und
// Mutantenlauf bleiben auf sonnet: ihre Rohausgabe geht wörtlich an die Linsen.
```
- **welle-mit-pruefung.js, Kopfkommentar** – hinter der Zeile `// und die laufen nacheinander; die Obergrenze von zwei hält executer-welle selbst.` und vor `// Verbrauch:` diese zwei Zeilen einfügen:
```
// Modell: werkzeug:kopie läuft auf haiku, weil nur das Skript die Ausgabe liest. werkzeug:vergleich
// bleibt auf sonnet: seine Rohausgabe geht an den pruefer.
```
Die Beschreibungen in `meta` (`description`, `phases`) nennen bei quorum.js „Codex über Hilfsagenten“; das bleibt.
## Fälle in faelle_workflows.mjs
Bestehende Erwartungen, die jetzt falsch sind, an die Tabelle anpassen (Grep nach `'sonnet'` bei den Labels `werkzeug:paket` und `codex:`, etwa Zeilen 1190 und 1201). Dazu neue Fälle, die das Modell **je Label** festnageln:
- quorum: Grundlinienlauf mit Codex – `werkzeug:paket`, jedes `codex:`-Label einschließlich `codex:ende` hat `modell === 'haiku'`, `effort === 'low'`, kein `agentType`; `zusammenfuehren` `sonnet`/`medium`.
- quorum: Lauf, in dem Codex abgebrochen wird (vorhandene Antwortvorgaben für Warten ohne Ende nutzen) – `codex:abbruch` ist `haiku`/`low`.
- plankritik: Lauf mit `materialisieren` und Mutanten an – `werkzeug:kopie` `haiku`/`low`, `werkzeug:planwerkzeuge` und `werkzeug:mutanten` `sonnet`/`low`.
- welle-mit-pruefung: Lauf ohne `kopie`, mit `ohne` und `suite` – `werkzeug:kopie` `haiku`/`low`, `werkzeug:vergleich` `sonnet`/`low`.
Ein neuer Fall braucht einen Rotlauf (`.vault-tools/workflow-pruefung/LIESMICH.md`, Ablauf 2): Lege mit `mktemp -d` einen Ordner an, schreibe die vier geänderten Workflows im Stand von `git -C D:/Claude show HEAD:.claude/workflows/<datei>` hinein und kopiere die übrigen `.js` aus `D:/Claude/.claude/workflows/` dazu. Fahre `node D:/Claude/.vault-tools/workflow-pruefung/faelle_workflows.mjs <ordner>`: Die neuen und geänderten Fälle müssen dort scheitern, alle übrigen grün bleiben.
## Mutanten in mutanten_workflows.py
Je Datei einen Mutanten in `MUTANTEN` ergänzen, der genau ein Modell zurückdreht, im Format der vorhandenen Einträge (Ersetzung muss genau einmal vorkommen):
- quorum: `codex:ende` zurück auf `sonnet`
- plankritik: Aufruf `werkzeug:kopie` übergibt `'sonnet'` statt `'haiku'`
- plankritik: Vorgabe `modell = 'sonnet'` → `modell = 'haiku'` (Planwerkzeuge und Mutantenlauf kippen)
- welle-mit-pruefung: Aufruf `werkzeug:kopie` übergibt `'sonnet'`
- welle-mit-pruefung: Vorgabe `modell = 'sonnet'` → `modell = 'haiku'`
- quorum: `model: 'haiku'` in `werkzeugAgent` → `'sonnet'`
Den vollen Mutantenlauf fährst du **nicht** (knapp 50 Minuten); das macht der Koordinator. Prüfe nur, dass jede neue Ersetzung genau einmal vorkommt: `grep -c` je Muster in der Zieldatei, Ausgabe in den Bericht.
## Prüfen
```bash
# Alle Fälle gegen die geänderten Workflows, Exit 0 erwartet
node D:/Claude/.vault-tools/workflow-pruefung/faelle_workflows.mjs
# Kein CR in den .js-Dateien (je 0)
for f in quorum plankritik welle-mit-pruefung executer-welle; do printf '%s ' $f; tr -cd '\r' < D:/Claude/.claude/workflows/$f.js | wc -c; done
# Modellstellen nachlesen
grep -n "model: \|modell = \|'haiku')" D:/Claude/.claude/workflows/quorum.js D:/Claude/.claude/workflows/plankritik.js D:/Claude/.claude/workflows/welle-mit-pruefung.js
# Mutanten-Datei kompiliert
python -I -X utf8 -m py_compile D:/Claude/.vault-tools/workflow-pruefung/mutanten_workflows.py
```
Berichte: Zahl der Fälle vor und nach der Änderung, Ergebnis, Rotlauf mit Zahl und Namen der dort gescheiterten Fälle, die `grep -c`-Werte der neuen Mutanten, `git -C D:/Claude diff --stat -- .claude/workflows .vault-tools/workflow-pruefung`, und jede Abweichung von diesem Task.
@@ -0,0 +1,303 @@
"""Fährt die Angriffsliste gegen shell_wache.py, je Fall als eigener Prozess.
Ersteller: Pascal Michel
Erstellt: 2026-10-30
Entwurf: ENTWURF.md im selben Ordner
Je Fall: Exit 0, stdout leer oder genau ein JSON-Objekt mit
hookSpecificOutput.hookEventName == "PreToolUse", ohne permissionDecision, die
Schlüssel in eckigen Klammern am Zeilenanfang von additionalContext sind genau
die erwarteten, Laufzeit unter 2 s. Der Hook läuft mit python -I -X utf8 und ohne
PYTHONIOENCODING in der Umgebung, wie Claude Code ihn startet.
Grundlinie zuerst: Ein gesunder Befehl muss still sein. Scheitert sie, bricht der
Treiber ab, weil der Rest nichts misst.
Aufruf:
python -I -X utf8 .vault-tools/shell-wache/faelle_shell_wache.py [Hook-Pfad]
"""
from __future__ import annotations
import json
import os
import re
import subprocess
import sys
import time
from pathlib import Path
HIER = Path(__file__).resolve().parent
MAX_DAUER = 2.0
ZEITLIMIT = 10
KOPFZEILE = "shell-wache warnt nur, der Befehl läuft trotzdem."
# (Name, stdin als Bytes, erwartete Schlüssel[, Text, der im Hinweis stehen muss])
Fall = tuple
def eingabe(cmd: str) -> bytes:
return json.dumps({"tool_name": "Bash", "tool_input": {"command": cmd}}).encode("utf-8")
def roh(daten: object) -> bytes:
return json.dumps(daten).encode("utf-8")
def treffer(gruppe: str, key: str, befehle: list[str]) -> list[Fall]:
return [(f"{gruppe} | Treffer | {b!r}", eingabe(b), {key}) for b in befehle]
def still(gruppe: str, befehle: list[str]) -> list[Fall]:
return [(f"{gruppe} | still | {b!r}", eingabe(b), set()) for b in befehle]
GRUNDLINIE: list[Fall] = [
("GRUNDLINIE | gesunder Befehl bleibt still", eingabe("ls -la && python -I -X utf8 x.py"), set()),
("GRUNDLINIE | python3 wird gemeldet", eingabe("python3 x.py"), {"python3"}),
]
PY_I_TREFFER = [
"python -I x.py",
"python -IB x.py",
"python -BI x.py",
'python -Ic "print(1)"',
'python -I -c "x" -X utf8',
"python -I -X utf8=0 x.py",
"/c/Py/python.exe -I x.py",
'"C:/Py/python.exe" -I x.py',
"cd a && python -I x.py",
"PYTHONIOENCODING=utf-8 python -I x.py",
]
PY_I_STILL = [
"python -I -X utf8 x.py",
"python -IXutf8 x.py",
"python -IX utf8 x.py",
"python -X utf8 -I -B x.py",
"python -I -X utf8=1 x.py",
"python x.py -I",
'python -c "import x" -I',
"ipython -I",
"python -m pytest -I",
]
PY3_TREFFER = ["python3 x.py", "python3.12 -m venv v", "ls; python3 -V", "python3.exe x.py"]
PY3_STILL = [
"python x.py",
"python3-config --libs",
"wsl -d Debian -- python3 x.py",
"ssh h python3 x.py",
"docker exec c python3 x.py",
]
BASH_TREFFER = [
'cmd //c "bash x.sh"',
'cmd.exe /c bash -c "x"',
"CMD /C BASH x",
"powershell -c \"bash -lc 'x'\"",
'powershell.exe -NoProfile -Command "& bash x"',
'pwsh -c "bash x"',
"python -X utf8 -c \"import subprocess; subprocess.run(['bash', '-c', 'x'])\"",
"python x.py <<'EOF'\nsubprocess.run([\"bash.exe\", \"x\"])\nEOF",
]
BASH_STILL = [
'bash -c "x"',
"bash x.sh",
'cmd //c "C:/Program Files/Git/bin/bash.exe" x',
'cmd //c "C:\\Program Files\\Git\\bin\\bash.exe" x',
"powershell -c \"wsl -d Debian -- bash -lc 'x'\"",
'cmd /c "ssh.exe -o A=B root@h bash -s < f.sh"',
'powershell -c "Get-Content ~/.bashrc"',
'powershell -c "x"\nbash y.sh',
"ssh h python - <<'EOF'\nsubprocess.run(['bash','x'])\nEOF",
"python -c \"subprocess.run([GITBASH, '-c'])\"",
]
PKILL_TREFFER = [
"pkill -f x",
'pkill -TERM -f "[s]leep 1"',
"pkill -fx x",
"pkill --full x",
"pkill -u root -f x",
"sudo pkill -f x",
"wsl -d Debian -- bash -c 'a; pkill -f \"y\"'",
"/usr/bin/pkill -f x",
]
PKILL_STILL = ["pkill x", "pkill -TERM x", "pgrep -f x", "pkill -u fx x"]
FAELLE: list[Fall] = []
FAELLE += treffer("python-I", "python-I", PY_I_TREFFER) + still("python-I", PY_I_STILL)
FAELLE += treffer("python3", "python3", PY3_TREFFER) + still("python3", PY3_STILL)
FAELLE += treffer("bash-windows", "bash-windows", BASH_TREFFER) + still("bash-windows", BASH_STILL)
FAELLE += treffer("pkill-f", "pkill-f", PKILL_TREFFER) + still("pkill-f", PKILL_STILL)
# Mehrere Muster in einem Befehl
FAELLE += [
(
"mehrere | alle vier Muster",
eingabe("python -I x.py; python3 y.py; cmd /c bash z; pkill -f w"),
{"python-I", "python3", "bash-windows", "pkill-f"},
),
("mehrere | dasselbe Muster zweimal", eingabe("python -I a.py; python -I b.py"), {"python-I"}),
]
# Reihenfolge: je Muster am Anfang, am Ende, in Zeile 3
for _key, _treffer in (
("python-I", "python -I x.py"),
("python3", "python3 x.py"),
("bash-windows", "cmd /c bash x"),
("pkill-f", "pkill -f x"),
):
FAELLE += [
(f"reihenfolge | {_key} am Anfang", eingabe(f"{_treffer}; ls"), {_key}),
(f"reihenfolge | {_key} am Ende", eingabe(f"ls; {_treffer}"), {_key}),
(f"reihenfolge | {_key} in Zeile 3", eingabe(f"ls\npwd\n{_treffer}"), {_key}),
]
# Eingabe
FAELLE += [
("eingabe | stdin leer", b"", {"eingabe"}, "stdin leer"),
("eingabe | kein JSON", b"nicht json", {"eingabe"}, "kein JSON)"),
("eingabe | Liste", b"[]", {"eingabe"}, "kein JSON-Objekt"),
("eingabe | null", b"null", {"eingabe"}, "kein JSON-Objekt"),
("eingabe | leeres Objekt", b"{}", {"eingabe"}, "tool_input fehlt oder ist kein Objekt"),
("eingabe | tool_input fehlt", roh({"tool_name": "Bash"}), {"eingabe"},
"tool_input fehlt oder ist kein Objekt"),
("eingabe | tool_input null", roh({"tool_name": "Bash", "tool_input": None}), {"eingabe"},
"tool_input fehlt oder ist kein Objekt"),
("eingabe | tool_input Zeichenkette", roh({"tool_name": "Bash", "tool_input": "x"}), {"eingabe"},
"tool_input fehlt oder ist kein Objekt"),
("eingabe | command fehlt", roh({"tool_name": "Bash", "tool_input": {}}), {"eingabe"},
"command fehlt oder ist keine Zeichenkette"),
("eingabe | command null", roh({"tool_name": "Bash", "tool_input": {"command": None}}), {"eingabe"},
"command fehlt oder ist keine Zeichenkette"),
("eingabe | command Zahl", roh({"tool_name": "Bash", "tool_input": {"command": 5}}), {"eingabe"},
"command fehlt oder ist keine Zeichenkette"),
("eingabe | command Liste", roh({"tool_name": "Bash", "tool_input": {"command": ["ls"]}}), {"eingabe"},
"command fehlt oder ist keine Zeichenkette"),
("eingabe | ungueltiges UTF-8 im Befehl",
b'{"tool_name":"Bash","tool_input":{"command":"ls \xff"}}', set()),
("eingabe | tool_name PowerShell mit python3",
roh({"tool_name": "PowerShell", "tool_input": {"command": "python3 x"}}), {"eingabe"},
"tool_name ist PowerShell, nicht Bash"),
("eingabe | command leer", eingabe(""), set()),
("eingabe | command nur Leerraum", eingabe(" "), set()),
("eingabe | zusaetzliche Felder",
roh({"tool_name": "Bash", "tool_use_id": "t1", "session_id": "s",
"tool_input": {"command": "ls", "description": "d", "timeout": 5}}), set()),
]
def schluessel_von(text: str) -> set[str]:
return set(re.findall(r"^\[([^\]]+)\]", text, flags=re.M))
def hat_schluessel(objekt: object, name: str) -> bool:
if isinstance(objekt, dict):
return name in objekt or any(hat_schluessel(v, name) for v in objekt.values())
if isinstance(objekt, list):
return any(hat_schluessel(v, name) for v in objekt)
return False
def fahre(hook: Path, stdin: bytes) -> tuple[int, str, str, float]:
umgebung = {k: v for k, v in os.environ.items() if k != "PYTHONIOENCODING"}
start = time.perf_counter()
lauf = subprocess.run(
[sys.executable, "-I", "-X", "utf8", str(hook)],
input=stdin,
capture_output=True,
timeout=ZEITLIMIT,
env=umgebung,
)
dauer = time.perf_counter() - start
try:
out = lauf.stdout.decode("utf-8")
except UnicodeDecodeError as exc:
out = "UNGUELTIGES-UTF-8: %s" % exc
return lauf.returncode, out, lauf.stderr.decode("utf-8", errors="replace"), dauer
def pruefe(hook: Path, fall: Fall) -> list[str]:
_, stdin, erwartet = fall[:3]
muss_enthalten = fall[3] if len(fall) > 3 else None
try:
code, out, err, dauer = fahre(hook, stdin)
except subprocess.TimeoutExpired:
return [f"Zeitlimit {ZEITLIMIT} s ueberschritten"]
probleme: list[str] = []
if code != 0:
probleme.append(f"Exit {code} statt 0")
if dauer >= MAX_DAUER:
probleme.append(f"Laufzeit {dauer:.2f} s, Grenze {MAX_DAUER:.0f} s")
if err.strip():
probleme.append(f"stderr nicht leer: {err.strip()[:200]!r}")
if not erwartet:
if out != "":
probleme.append(f"stdout nicht leer: {out[:300]!r}")
return probleme
try:
daten = json.loads(out)
except ValueError:
return probleme + [f"stdout ist kein JSON: {out[:300]!r}"]
if not isinstance(daten, dict) or set(daten) != {"hookSpecificOutput"}:
return probleme + [f"Objekt hat nicht genau den Schluessel hookSpecificOutput: {out[:300]!r}"]
if hat_schluessel(daten, "permissionDecision"):
probleme.append("permissionDecision kommt vor")
innen = daten["hookSpecificOutput"]
if not isinstance(innen, dict) or innen.get("hookEventName") != "PreToolUse":
return probleme + ["hookEventName ist nicht PreToolUse"]
text = innen.get("additionalContext")
if not isinstance(text, str) or not text:
return probleme + ["additionalContext fehlt oder ist leer"]
if not text.startswith(KOPFZEILE + "\n"):
probleme.append(f"Kopfzeile fehlt: {text[:80]!r}")
gefunden = schluessel_von(text)
if gefunden != erwartet:
probleme.append(f"Schluessel {sorted(gefunden)} statt {sorted(erwartet)}")
if muss_enthalten and muss_enthalten not in text:
probleme.append(f"Hinweis ohne {muss_enthalten!r}: {text!r}")
return probleme
def main() -> int:
argumente = [a for a in sys.argv[1:] if not a.startswith("--")]
hook = Path(argumente[0]).resolve() if argumente else HIER / "shell_wache.py"
if not hook.is_file():
print(f"ABBRUCH: {hook} fehlt")
return 1
print(f"Hook: {hook}")
for fall in GRUNDLINIE:
try:
probleme = pruefe(hook, fall)
except Exception as exc:
probleme = [f"ABSTURZ im Treiber: {type(exc).__name__}: {exc}"]
print(f"{'OK' if not probleme else 'FEHLER':7} {fall[0]}" + "".join(f"\n {p}" for p in probleme))
if probleme:
print("ABBRUCH: Die Grundlinie ist nicht gruen. Der Rest misst nichts.")
return 1
ergebnisse: list[bool] = []
for fall in FAELLE:
try:
probleme = pruefe(hook, fall)
except Exception as exc:
probleme = [f"ABSTURZ im Treiber: {type(exc).__name__}: {exc}"]
ergebnisse.append(not probleme)
print(f"{'OK' if not probleme else 'FEHLER':7} {fall[0]}" + "".join(f"\n {p}" for p in probleme))
fehler = ergebnisse.count(False)
print()
print(f"{len(ergebnisse)} Faelle, {fehler} Fehler (zusaetzlich {len(GRUNDLINIE)} Grundlinie)")
return 0 if fehler == 0 else 1
if __name__ == "__main__":
try:
sys.stdout.reconfigure(encoding="utf-8")
except AttributeError:
pass
try:
code = main()
except Exception as exc:
print(f"FEHLER Treiber abgestuerzt: {type(exc).__name__}: {exc}")
code = 1
raise SystemExit(code)
+293
View File
@@ -0,0 +1,293 @@
"""PreToolUse-Hook auf das Bash-Werkzeug: warnt vor vier bekannten Windows-Shell-Fallen, lehnt nie ab.
Ersteller: Pascal Michel
Erstellt: 2026-10-30
Entwurf: ENTWURF.md im selben Ordner · Doku: docs/code/shell_wache.md
GELTUNGSBEREICH
Gelesen wird tool_input.command der Hook-Eingabe auf stdin (Bytes, UTF-8 mit
Ersatzzeichen). Geprüft werden genau vier Muster, ein Hinweis je getroffenem
Muster, auch wenn es mehrfach trifft:
[python-I] python/python.exe mit -I, aber ohne -X utf8 oder -X utf8=1.
Optionen werden bis zum ersten Nicht-Optionswort gelesen;
-c und -m beenden sie, auch im Bündel (-Ic); -W und -X nehmen
ein Argument, angehängt oder als nächstes Wort. -X utf8=0
zählt nicht als utf8.
[python3] python3, python3.<n>, mit oder ohne .exe.
[bash-windows] bash/bash.exe ohne Pfad im Windows-Kontext: (a) hinter cmd /c,
cmd //c, cmd.exe /c, powershell, pwsh bis zum Zeilenende,
(b) als Python-Listenelement ["bash" oder ['bash.exe' im
ganzen Befehl. Steht direkt davor / oder \\, bleibt es still.
[pkill-f] pkill mit --full oder einem Bündel mit kleinem f, gelesen wie
bei python; -u -U -g -G -P -s -t -F und --signal nehmen das
nächste Wort als Argument. Auch in WSL und über ssh.
Linux-Weiche: ein Wort wsl, ssh, docker, podman oder kubectl (mit oder ohne
.exe). python3 und Muster (b) bleiben im ganzen Befehl still, sobald irgendwo
eine Weiche steht; Muster (a) nur für ein bash hinter einer Weiche auf
derselben Zeile. python -I und pkill kennen keine Weiche.
Groß- und Kleinschreibung: cmd, powershell, pwsh, bash, python, Weichenwörter
ohne Rücksicht darauf; pkill und alle Optionen genau.
Ein Programmwort beginnt am Befehlsanfang oder nach Leerraum oder einem der
Zeichen ; & | ( ` ' " = und darf einen Pfad vor sich tragen (außer bash).
Ausgabe: ohne Befund nichts. Mit Befund genau ein JSON-Objekt
{"hookSpecificOutput": {"hookEventName": "PreToolUse", "additionalContext": ...}}.
Nie permissionDecision, nie decision, nie continue. Exit ist immer 0.
Kaputte Eingabe (leer, kein JSON-Objekt, tool_input oder command fehlt oder
falscher Typ, tool_name ungleich Bash) ist ein Befund [eingabe], ein interner
Fehler ein Befund [intern]; leerer command bleibt still.
BENANNTE LÜCKEN
* Erwähnungen in Zeichenketten schlagen an, etwa grep "python -I" datei oder eine
Commit-Nachricht. Der Hook kennt keine Shell-Grammatik.
* Das PowerShell-Werkzeug ist nicht abgedeckt; dort ist jeder Befehl
Windows-Kontext. Eine Erweiterung bräuchte eine eigene Kontextregel.
* py (Python-Launcher), subprocess mit shell=True und Skriptdateien, die erst zur
Laufzeit gelesen werden, sieht der Hook nicht.
* Ein Befehl, der ein lokales python3 und zugleich ssh enthält, bleibt still
(die Weiche gilt für den ganzen Befehl).
* Workflow-Subagenten laufen mit derselben settings.json; der Hook wirkt dort
mit. Ob ihr Kontext den Hinweis zeigt, belegt erst ein Lauf.
* Backslashes in Heredocs werden nicht geprüft (ENTWURF.md, Frage 7).
DIESER HOOK IST EIN FANGNETZ, KEIN NACHWEIS. Er erkennt Muster im Befehlstext,
nicht, was der Befehl tut. Ein Fehlalarm kostet eine Zeile Kontext, keinen
abgelehnten Befehl.
Exit ist immer 0. Exit 2 wäre bei PreToolUse eine Ablehnung (Hilfetext von Claude
Code 2.1.309), andere Codes erreichten das Modell nicht (ENTWURF.md, Frage 4).
"""
from __future__ import annotations
import json
import re
import sys
KOPFZEILE = "shell-wache warnt nur, der Befehl läuft trotzdem."
HINWEISE = {
"python-I": (
"[python-I] python -I ohne -X utf8: -I ignoriert PYTHONIOENCODING, Umlaute brechen "
"unter cp1252. Nimm python -I -X utf8 (AI/Regelwerk/windows-shell.md, „Welche Shell, welches Python“)."
),
"python3": (
"[python3] python3 ist unter Windows der Microsoft-Store-Stub. Nimm python "
"(AI/Regelwerk/windows-shell.md, „Welche Shell, welches Python“)."
),
"bash-windows": (
"[bash-windows] bash ohne Pfad aus cmd, PowerShell oder Python startet unter Windows "
"den WSL-Starter. Git-Bash mit vollem Pfad C:\\Program Files\\Git\\bin\\bash.exe, "
"WSL mit wsl -d <Distro> (AI/Regelwerk/windows-shell.md, „Welche Shell, welches Python“)."
),
"pkill-f": (
"[pkill-f] pkill -f trifft jede Befehlszeile mit dem Muster, auch die eigene Shell "
"und fremde Läufe in WSL. Erst pgrep -af lesen, dann gezielt kill <pid> oder ein "
"Muster wie \"[s]leep 7199\" (Erinnerung pkill-f-trifft-die-eigene-shell; "
"AI/Regelwerk/windows-shell.md)."
),
}
# Ein Programmwort beginnt am Zeilen- oder Befehlsanfang oder nach einem dieser Zeichen.
_START = r"(?:^|(?<=[\s;&|(`'\"=]))"
_PFAD = r"(?:[^\s;&|()`'\"=]*[/\\])?"
_ENDE = r"(?=$|[\s'\";)|&])"
def _programm(name: str, pfad: bool = True) -> str:
return _START + (_PFAD if pfad else "") + "(?:" + name + ")" + _ENDE
_RE_PYTHON = re.compile(_programm(r"python(?:\.exe)?"), re.I | re.M)
_RE_PYTHON3 = re.compile(_programm(r"python3(?:\.\d+)?(?:\.exe)?"), re.I | re.M)
_RE_PKILL = re.compile(_programm("pkill"), re.M)
_RE_WEICHE = re.compile(_programm(r"(?:wsl|ssh|docker|podman|kubectl)(?:\.exe)?"), re.I | re.M)
_RE_BASH = re.compile(_programm(r"bash(?:\.exe)?", pfad=False), re.I | re.M)
_RE_KONTEXT = re.compile(
_programm(r"cmd(?:\.exe)?[ \t]+//?c|(?:powershell|pwsh)(?:\.exe)?"), re.I | re.M
)
_RE_LISTE_BASH = re.compile(r"\[\s*[\"']bash(?:\.exe)?[\"']", re.I)
# Buchstaben, deren Option das nächste Wort als Argument nimmt.
_PKILL_MIT_ARGUMENT = "uUgGPstF"
def lies_stdin() -> bytes:
if sys.stdin is None or sys.stdin.isatty():
return b""
return sys.stdin.buffer.read()
def lies_eingabe(roh: bytes) -> tuple[str | None, str]:
"""(Grund, Befehl): Grund ist None, wenn der Befehl geprüft werden kann."""
text = roh.decode("utf-8", "replace").lstrip("\ufeff")
if not text.strip():
return "stdin leer", ""
try:
daten = json.loads(text)
except ValueError:
return "kein JSON", ""
if not isinstance(daten, dict):
return "kein JSON-Objekt", ""
name = daten.get("tool_name")
if name is not None and name != "Bash":
return f"tool_name ist {name}, nicht Bash", ""
eingabe = daten.get("tool_input")
if not isinstance(eingabe, dict):
return "tool_input fehlt oder ist kein Objekt", ""
befehl = eingabe.get("command")
if not isinstance(befehl, str):
return "command fehlt oder ist keine Zeichenkette", ""
return None, befehl
def _rest_der_zeile(befehl: str, ab: int) -> list[str]:
# Rest der Zeile hinter dem Programmwort, bis zum ersten ; & |, als Wörter ohne Anführungszeichen.
rest = befehl[ab:].split("\n", 1)[0]
rest = re.split(r"[;&|]", rest, maxsplit=1)[0].lstrip(" \t'\"")
return [w.strip("'\")") for w in rest.split()]
def python_i_ohne_utf8(woerter: list[str]) -> bool:
ist_i = False
utf8 = False
i = 0
while i < len(woerter):
w = woerter[i]
i += 1
if not w.startswith("-") or w in ("-", "--"):
break
if w.startswith("--"):
continue
buendel = w[1:]
ende = False
for pos, zeichen in enumerate(buendel):
if zeichen == "I":
ist_i = True
elif zeichen in "cm":
ende = True
break
elif zeichen in "WX":
arg = buendel[pos + 1:]
if not arg and i < len(woerter):
arg = woerter[i]
i += 1
if zeichen == "X":
utf8 = arg in ("utf8", "utf8=1")
break
if ende:
break
return ist_i and not utf8
def pkill_mit_f(woerter: list[str]) -> bool:
i = 0
while i < len(woerter):
w = woerter[i]
i += 1
if not w.startswith("-") or w in ("-", "--"):
break
if w.startswith("--"):
if w == "--full":
return True
if w == "--signal":
i += 1
continue
buendel = w[1:]
for pos, zeichen in enumerate(buendel):
if zeichen == "f":
return True
if zeichen in _PKILL_MIT_ARGUMENT:
if pos == len(buendel) - 1:
i += 1
break
return False
def erkenne_python_i(befehl: str) -> bool:
return any(python_i_ohne_utf8(_rest_der_zeile(befehl, m.end())) for m in _RE_PYTHON.finditer(befehl))
def erkenne_python3(befehl: str) -> bool:
if _RE_WEICHE.search(befehl):
return False
return _RE_PYTHON3.search(befehl) is not None
def _bash_im_kontext(befehl: str) -> bool:
for zeile in befehl.split("\n"):
kontext = _RE_KONTEXT.search(zeile)
if not kontext:
continue
rest = zeile[kontext.end():]
weiche = _RE_WEICHE.search(rest)
for bash in _RE_BASH.finditer(rest):
if weiche is None or bash.start() < weiche.start():
return True
return False
def erkenne_bash_windows(befehl: str) -> bool:
if _bash_im_kontext(befehl):
return True
if _RE_WEICHE.search(befehl):
return False
return _RE_LISTE_BASH.search(befehl) is not None
def erkenne_pkill_f(befehl: str) -> bool:
return any(pkill_mit_f(_rest_der_zeile(befehl, m.end())) for m in _RE_PKILL.finditer(befehl))
# Reihenfolge ist die Reihenfolge der Zeilen im Hinweis.
DETEKTOREN = (
("python-I", erkenne_python_i),
("python3", erkenne_python3),
("bash-windows", erkenne_bash_windows),
("pkill-f", erkenne_pkill_f),
)
def baue_text(schluessel: list[str], grund: str | None, intern: list[str]) -> str:
zeilen = [KOPFZEILE]
zeilen += [HINWEISE[k] for k in schluessel]
if grund is not None:
zeilen.append(f"[eingabe] Hook-Eingabe unbrauchbar ({grund}), nichts geprüft.")
if intern:
zeilen.append(
f"[intern] shell-wache ist abgestürzt ({', '.join(intern)}), Prüfung unvollständig."
)
return "\n".join(zeilen)
def melde(text: str) -> None:
ausgabe = {"hookSpecificOutput": {"hookEventName": "PreToolUse", "additionalContext": text}}
sys.stdout.write(json.dumps(ausgabe, ensure_ascii=True))
sys.stdout.flush()
def main() -> None:
schluessel: list[str] = []
intern: list[str] = []
grund: str | None = None
try:
grund, befehl = lies_eingabe(lies_stdin())
if grund is None and befehl.strip():
for name, detektor in DETEKTOREN:
try:
if detektor(befehl):
schluessel.append(name)
except Exception as fehler:
intern.append(type(fehler).__name__)
except BaseException as fehler:
intern.append(type(fehler).__name__)
if schluessel or grund is not None or intern:
melde(baue_text(schluessel, grund, intern))
if __name__ == "__main__":
try:
main()
except BaseException:
# Letzte Schranke: Exit 0 ist Pflicht, sonst liest Claude Code stdout nicht.
pass
sys.exit(0)