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:
1 parent
f2ccec39d6
commit
13213ea0ac
46 files changed
+2380
-48
No files matched your search
@@ -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.
|
||||
+115
@@ -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)
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user