"""Fährt die Angriffsliste gegen regelwerk_nach_edit.py, je Fall auf einem Wegwerf-Vault. Ersteller: Pascal Michel Erstellt: 2026-09-13 Entwurf: ENTWURF.md im selben Ordner Statt des echten regelwerk_check.py liegt im Wegwerf-Vault eine Attrappe. Sie schreibt ihren Aufruf nach aufruf.json und zeigt dann ein festgelegtes Verhalten: grün, Befund, Abbruch, ACHTUNG, stumm, Absturz, hängt, sehr viel Ausgabe. Ihre Zeilen haben die Form der echten Ausgabe (regelwerk_check.py Zeilen 114, 368, 370 und 372). So lässt sich je Fall unterscheiden, ob der Hook still blieb, ohne die Prüfung zu starten, oder still blieb, nachdem sie grün war. Grundlinie zuerst: Ein grüner Check muss still bleiben und nachweislich gelaufen sein, ein Befund muss gemeldet werden. Sonst misst der Rest nichts. Seit der Nachbesserung Runde 2 bestimmt regelwerk_nach_edit.py seine Vault-Wurzel ausschließlich aus dem eigenen Skriptpfad, das Feld cwd der Hook-Eingabe zählt nicht mehr (Prüferbefund Runde 2, Koordinatorentscheidung). lauf_fall() kopiert den Hook deshalb für jeden Fall zuerst in den Wegwerf- Vault, statt das Original mit einem cwd-Feld dorthin zu verweisen. Aufruf: python .vault-tools/session-hooks/faelle_nach_edit.py python .vault-tools/session-hooks/faelle_nach_edit.py --echt """ from __future__ import annotations import contextlib import importlib.util import io import json import os import re import shutil import subprocess import sys import tempfile import time from dataclasses import dataclass, field from pathlib import Path from typing import Callable HIER = Path(__file__).resolve().parent HOOK = HIER / "regelwerk_nach_edit.py" MAX_KONTEXT = 4100 FAKE_KOPF = '''import json import sys import time from pathlib import Path Path(__file__).resolve().parents[2].joinpath("aufruf.json").write_text( json.dumps({"argv": sys.argv[1:], "exe": sys.executable}), encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8") ''' FAKES = { "gruen": FAKE_KOPF + '''print() print("Keine der 6 gelaufenen Prüfungen hat angeschlagen.") sys.exit(0) ''', "befund": FAKE_KOPF + '''print("[klein] AI/Regelwerk/regel.md — status: pending-review — Freigabe steht aus") print() print("1 Befund(e) aus 6 von 6 Prüfungen.") sys.exit(1) ''', "abbruch": FAKE_KOPF + '''print("[blockierend] regelwerk_check.py — Abbruch: KeyError: 'x'") print() print("1 Befund(e) aus 2 von 6 Prüfungen.") sys.exit(2) ''', "achtung": FAKE_KOPF + '''print() print("Keine der 5 gelaufenen Prüfungen hat angeschlagen.") print("ACHTUNG: nur 5 von 6 Prüfungen sind gelaufen — der Rest hatte keine Voraussetzung.") sys.exit(0) ''', "stumm": FAKE_KOPF + '''sys.exit(0) ''', "absturz": FAKE_KOPF + '''raise RuntimeError("Absturz im Check") ''', "haengt": FAKE_KOPF + '''time.sleep(120) ''', "viel": FAKE_KOPF + '''for i in range(200): print("[wichtig] AI/Regelwerk/regel.md — toter Wikilink: [[ziel%d]]" % i) print() print("200 Befund(e) aus 6 von 6 Prüfungen.") sys.exit(1) ''', } def schreibe(vault: Path, rel: str, inhalt: str | bytes) -> None: # Immer Bytes: write_text würde unter Windows jedes LF zu CRLF machen. pfad = vault / rel pfad.parent.mkdir(parents=True, exist_ok=True) pfad.write_bytes(inhalt if isinstance(inhalt, bytes) else inhalt.encode("utf-8")) def baue_vault(v: Path, fake: str | None) -> None: schreibe(v, "CLAUDE.md", "# Stamm\n") schreibe(v, "AI/Regelwerk/regel.md", "---\nstatus: reference\n---\n") schreibe(v, "AI/Regelwerk/unter/tief.md", "x\n") schreibe(v, ".claude/agents/planer.md", "---\nname: planer\n---\n") schreibe(v, ".claude/skills/demo/SKILL.md", "---\nname: demo\n---\n") schreibe(v, "Inbox/notiz.md", "x\n") schreibe(v, "Code/CLAUDE.md", "x\n") if fake is not None: schreibe(v, ".vault-tools/regelwerk-check/regelwerk_check.py", FAKES[fake]) def vorwaerts(pfad: Path) -> str: return str(pfad).replace("\\", "/") def laufwerk_getauscht(pfad: Path) -> str: s = str(pfad) if re.match(r"^[A-Za-z]:", s): return s[0].swapcase() + s[1:] return s def j(daten: object) -> bytes: return json.dumps(daten).encode("utf-8") def edit(v: Path, datei: object, **extra: object) -> bytes: daten: dict[str, object] = { "session_id": "faelle", "transcript_path": "", "cwd": str(v), "permission_mode": "default", "hook_event_name": "PostToolUse", "tool_name": "Edit", "tool_input": {"file_path": datei}, "tool_response": {}, } daten.update(extra) return j(daten) def ohne_cwd(v: Path, datei: object) -> bytes: daten = json.loads(edit(v, datei)) del daten["cwd"] return j(daten) @dataclass class Fall: name: str fake: str | None stdin: Callable[[Path], bytes] erwartung: str enthaelt: list[str] = field(default_factory=list) system_enthaelt: str = "" max_dauer: float = 10.0 # None: Subprozess erbt die Umgebung dieses Treibers unveraendert (bisheriges # Verhalten). Gesetzt: ersetzt die Umgebung vollstaendig (Pruefbefunde Runde 2). umgebung: Callable[[Path], dict[str, str]] | None = None BEFUND = ["[klein] AI/Regelwerk/regel.md"] BEFUND_SYSTEM = "Exit 1, 1 Befundzeile(n)" def still(name: str, stdin: Callable[[Path], bytes]) -> Fall: return Fall(name, "befund", stdin, "still-ohne-lauf") def meldet(name: str, stdin: Callable[[Path], bytes]) -> Fall: return Fall(name, "befund", stdin, "meldung", BEFUND, BEFUND_SYSTEM) def cwd_zeigt_anderswo(v: Path) -> bytes: """cwd zeigt auf einen anderen, echten (leeren) Ordner. Seit der Nachbesserung Runde 2 liest der Hook cwd gar nicht mehr - die Wurzel ist ausschliesslich der Ordner, in dem der Hook selbst liegt (hier: v, weil lauf_fall() ihn dorthin kopiert). Das Ergebnis muss trotzdem melden, nicht still bleiben (Pruefbefund Runde 2).""" anderswo = v.parent / "anderswo" anderswo.mkdir(parents=True, exist_ok=True) return edit(v, str(v / "AI" / "Regelwerk" / "regel.md"), cwd=str(anderswo)) def ohne_pythonioencoding(_: Path) -> dict[str, str]: """Claude Code setzt PYTHONIOENCODING beim Hook-Aufruf nicht; nur die globale Planvorgabe setzt sie vor dem Aufruf dieses Treibers selbst (Pruefbefund Runde 2).""" umgebung = dict(os.environ) umgebung.pop("PYTHONIOENCODING", None) return umgebung GRUNDLINIE = [ Fall("GRUNDLINIE | gruener Check bleibt still und lief", "gruen", lambda v: edit(v, str(v / "CLAUDE.md")), "still-nach-lauf"), Fall("GRUNDLINIE | Befund wird gemeldet", "befund", lambda v: edit(v, str(v / "CLAUDE.md")), "meldung", BEFUND, BEFUND_SYSTEM), ] FAELLE = [ still("leer | stdin leer", lambda v: b""), still("leer | stdin nur Leerraum", lambda v: b" \r\n"), still("falscher Typ| stdin kein JSON", lambda v: b"kein json"), still("kodierung | stdin ungueltiges UTF-8", lambda v: b"\xff\xfe\x00"), still("falscher Typ| stdin ist eine Liste", lambda v: j([json.loads(edit(v, str(v / "CLAUDE.md")))])), still("falscher Typ| stdin ist eine Zahl", lambda v: b"5"), still("fehlt | tool_input fehlt", lambda v: j({"cwd": str(v), "tool_name": "Edit"})), still("null | tool_input ist null", lambda v: edit(v, "x", tool_input=None)), still("falscher Typ| tool_input ist eine Liste", lambda v: edit(v, "x", tool_input=[str(v / "CLAUDE.md")])), still("fehlt | file_path fehlt", lambda v: edit(v, "x", tool_input={"content": "a"})), still("null | file_path ist null", lambda v: edit(v, None)), still("falscher Typ| file_path ist eine Zahl", lambda v: edit(v, 5)), still("falscher Typ| file_path ist eine Liste", lambda v: edit(v, [str(v / "CLAUDE.md")])), still("leer | file_path ist leer", lambda v: edit(v, "")), still("wertebereich| file_path mit Nullzeichen", lambda v: edit(v, str(v / "CLAUDE.md") + "\u0000")), still("zusaetzlich | Pfad ausserhalb des Vaults", lambda v: edit(v, str(v.parent / "anderswo" / "CLAUDE.md"))), still("verweis | Nachbarordner mit gleichem Namensanfang", lambda v: edit(v, str(v) + "-kopie\\CLAUDE.md")), still("verweis | Datei neben dem Vault mit gleichem Namensanfang", lambda v: edit(v, str(v) + "-CLAUDE.md")), still("zusaetzlich | im Vault, ausserhalb des Geltungsbereichs", lambda v: edit(v, str(v / "Inbox" / "notiz.md"))), still("zusaetzlich | CLAUDE.md in einem Unterordner", lambda v: edit(v, str(v / "Code" / "CLAUDE.md"))), still("verweis | Ordnername mit gleichem Anfang", lambda v: edit(v, str(v / "AI" / "RegelwerkAlt" / "x.md"))), still("zusaetzlich | settings.json unter .claude", lambda v: edit(v, str(v / ".claude" / "settings.json"))), still("wertebereich| der Regelwerk-Ordner selbst", lambda v: edit(v, str(v / "AI" / "Regelwerk"))), still("verweis | .. fuehrt aus dem Regelwerk hinaus", lambda v: edit(v, vorwaerts(v) + "/AI/Regelwerk/../../Inbox/notiz.md")), still("verweis | .. fuehrt aus dem Vault hinaus", lambda v: edit(v, vorwaerts(v) + "/../fremd/AI/Regelwerk/x.md")), Fall("gruen | CLAUDE.md, Check ohne Befund", "gruen", lambda v: edit(v, str(v / "CLAUDE.md")), "still-nach-lauf"), Fall("gruen | Skill, Check ohne Befund", "gruen", lambda v: edit(v, str(v / ".claude" / "skills" / "demo" / "SKILL.md")), "still-nach-lauf"), meldet("pfad | Schraegstriche vorwaerts", lambda v: edit(v, vorwaerts(v) + "/CLAUDE.md")), meldet("pfad | Backslashes", lambda v: edit(v, str(v) + "\\AI\\Regelwerk\\regel.md")), meldet("pfad | Laufwerksbuchstabe in anderer Schreibung", lambda v: edit(v, laufwerk_getauscht(v / "AI" / "Regelwerk" / "regel.md"))), meldet("pfad | ganzer Pfad in Grossbuchstaben", lambda v: edit(v, str(v / "AI" / "Regelwerk" / "regel.md").upper())), meldet("pfad | Unterordner im Regelwerk", lambda v: edit(v, str(v / "AI" / "Regelwerk" / "unter" / "tief.md"))), meldet("pfad | Agentendatei", lambda v: edit(v, str(v / ".claude" / "agents" / "planer.md"))), meldet("pfad | Skilldatei", lambda v: edit(v, str(v / ".claude" / "skills" / "demo" / "SKILL.md"))), meldet("verweis | neue Datei, die es noch nicht gibt", lambda v: edit(v, str(v / "AI" / "Regelwerk" / "neu.md"))), meldet("pfad | relativer Pfad", lambda v: edit(v, "AI/Regelwerk/regel.md")), meldet("verweis | .. fuehrt ins Regelwerk hinein", lambda v: edit(v, vorwaerts(v) + "/Inbox/../AI/Regelwerk/regel.md")), meldet("zusaetzlich | Write statt Edit", lambda v: edit(v, str(v / "CLAUDE.md"), tool_name="Write")), meldet("zusaetzlich | MultiEdit", lambda v: edit(v, str(v / "CLAUDE.md"), tool_name="MultiEdit")), meldet("kodierung | ungueltiges UTF-8 im Pfad", lambda v: edit(v, "PLATZHALTER").replace( b"PLATZHALTER", vorwaerts(v).encode("utf-8") + b"/AI/Regelwerk/regel\xff.md")), Fall("umgebung | ungueltiges UTF-8 im Pfad bleibt gueltiges JSON ohne PYTHONIOENCODING", "befund", lambda v: edit(v, "PLATZHALTER").replace( b"PLATZHALTER", vorwaerts(v).encode("utf-8") + b"/AI/Regelwerk/regel\xff.md"), "meldung", enthaelt=BEFUND, system_enthaelt=BEFUND_SYSTEM, umgebung=ohne_pythonioencoding), Fall("umgebung | Nicht-ASCII-Pfad ohne PYTHONIOENCODING bleibt gueltiges UTF-8-JSON", "befund", lambda v: edit(v, str(v / "AI" / "Regelwerk" / "Prüfüng Übersicht 🚀.md")), "meldung", enthaelt=BEFUND, system_enthaelt=BEFUND_SYSTEM, umgebung=ohne_pythonioencoding), meldet("pfad | cwd mit abschliessendem Backslash", lambda v: edit(v, str(v / "CLAUDE.md"), cwd=str(v) + "\\")), meldet("fehlt | cwd fehlt, wird nicht mehr gelesen", lambda v: ohne_cwd(v, str(v / "CLAUDE.md"))), meldet("null | cwd ist null, wird nicht mehr gelesen", lambda v: edit(v, str(v / "CLAUDE.md"), cwd=None)), meldet("falscher Typ| cwd ist eine Zahl, wird nicht mehr gelesen", lambda v: edit(v, str(v / "CLAUDE.md"), cwd=5)), meldet("verweis | cwd existiert nicht, wird nicht mehr gelesen", lambda v: edit(v, str(v / "CLAUDE.md"), cwd=str(v / "gibtsnicht"))), meldet("verweis | cwd zeigt auf einen anderen Ordner, Ergebnis bleibt der Skript-Vault", cwd_zeigt_anderswo), Fall("check | Exit 2, Abbruch im Check", "abbruch", lambda v: edit(v, str(v / "CLAUDE.md")), "meldung", ["Abbruch: KeyError"], "Exit 2, 1 Befundzeile(n)"), Fall("check | Exit 0 mit ACHTUNG", "achtung", lambda v: edit(v, str(v / "CLAUDE.md")), "meldung", ["ACHTUNG: nur 5 von 6"], "Exit 0, 0 Befundzeile(n)"), Fall("check | Exit 0 ohne Schlusszeile", "stumm", lambda v: edit(v, str(v / "CLAUDE.md")), "meldung", ["(keine Ausgabe)"], "Exit 0, 0 Befundzeile(n)"), Fall("check | Check stuerzt ab", "absturz", lambda v: edit(v, str(v / "CLAUDE.md")), "meldung", ["--- stderr ---", "RuntimeError: Absturz im Check"], "Exit 1, 0 Befundzeile(n)"), Fall("umgebung | regelwerk_check.py fehlt", None, lambda v: edit(v, str(v / "CLAUDE.md")), "meldung", ["regelwerk_check.py fehlt"], "Regelwerk-Check fehlt"), Fall("umgebung | regelwerk_check.py haengt", "haengt", lambda v: edit(v, str(v / "CLAUDE.md")), "meldung", ["länger als 20 s"], "nach 20 s abgebrochen", max_dauer=28.0), Fall("wertebereich| sehr lange Ausgabe wird gekuerzt", "viel", lambda v: edit(v, str(v / "CLAUDE.md")), "meldung", ["weitere Zeilen gekürzt"], "Exit 1, 200 Befundzeile(n)"), ] def streng_utf8(roh: bytes) -> str: # stdout muss gueltiges UTF-8 sein. Ein ungueltiges Byte darf nicht still # ersetzt werden, sonst besteht der Umgebungsfall auch dann, wenn der Hook # cp1252 schreibt (Pruefrunde 2, Befund zur Dekodierung). try: return roh.decode("utf-8") except UnicodeDecodeError as exc: return "UNGUELTIGES-UTF-8: %s" % exc def fahre( skript: Path, eingabe: bytes, arbeitsordner: Path, umgebung: dict[str, str] | None = None, ) -> tuple[int, str, str, float]: start = time.perf_counter() lauf = subprocess.run( [sys.executable, str(skript)], input=eingabe, capture_output=True, cwd=str(arbeitsordner), timeout=90, env=umgebung, ) dauer = time.perf_counter() - start return ( lauf.returncode, streng_utf8(lauf.stdout), lauf.stderr.decode("utf-8", errors="replace"), dauer, ) def pruefe_aufruf(aufruf: Path, v: Path) -> list[str]: if not aufruf.exists(): return ["Check wurde nicht gestartet"] daten = json.loads(aufruf.read_text(encoding="utf-8")) probleme: list[str] = [] if daten.get("argv") != ["--vault", str(v)]: probleme.append(f"Aufruf-Argumente {daten.get('argv')!r} statt {['--vault', str(v)]!r}") if os.path.normcase(str(daten.get("exe"))) != os.path.normcase(sys.executable): probleme.append(f"Interpreter {daten.get('exe')!r} statt {sys.executable!r}") return probleme def pruefe_meldung(fall: Fall, out: str) -> list[str]: roh = out.strip() if not (roh.startswith("{") and roh.endswith("}")): return [f"stdout ist kein JSON-Objekt: {out[:300]!r}"] try: daten = json.loads(roh) except ValueError as exc: return [f"stdout nicht parsebar: {exc}"] if not isinstance(daten, dict) or set(daten) != {"hookSpecificOutput", "systemMessage"}: art = sorted(daten) if isinstance(daten, dict) else type(daten).__name__ return [f"falsche Schluessel: {art}"] innen = daten["hookSpecificOutput"] system = daten["systemMessage"] if not isinstance(innen, dict) or innen.get("hookEventName") != "PostToolUse": return ["hookSpecificOutput.hookEventName ist nicht PostToolUse"] kontext = innen.get("additionalContext") if not isinstance(kontext, str) or not kontext: return ["additionalContext fehlt oder ist leer"] probleme: list[str] = [] if not isinstance(system, str) or not system or "\n" in system: probleme.append(f"systemMessage ist keine einzelne Zeile: {system!r}") elif fall.system_enthaelt not in system: probleme.append(f"systemMessage ohne {fall.system_enthaelt!r}: {system!r}") probleme += [f"Kontext ohne {t!r}" for t in fall.enthaelt if t not in kontext] if len(kontext) > MAX_KONTEXT: probleme.append(f"Kontext hat {len(kontext)} Zeichen, Grenze {MAX_KONTEXT}") if probleme: probleme.append("Kontext war:\n" + kontext) return probleme def pruefe(fall: Fall, v: Path, code: int, out: str, dauer: float) -> list[str]: probleme: list[str] = [] if code != 0: probleme.append(f"Exit {code} statt 0") if dauer > fall.max_dauer: probleme.append(f"Laufzeit {dauer:.1f} s ueber {fall.max_dauer:.0f} s") aufruf = v / "aufruf.json" if fall.erwartung == "still-ohne-lauf": if out != "": probleme.append(f"stdout nicht leer: {out[:300]!r}") if aufruf.exists(): probleme.append("Check wurde gestartet, obwohl er nicht haette laufen duerfen") elif fall.erwartung == "still-nach-lauf": if out != "": probleme.append(f"stdout nicht leer: {out[:300]!r}") probleme += pruefe_aufruf(aufruf, v) else: probleme += pruefe_meldung(fall, out) if fall.fake is not None: probleme += pruefe_aufruf(aufruf, v) return probleme def lauf_fall(fall: Fall, arbeit: Path) -> tuple[bool, str]: v = arbeit / "fall" if v.exists(): shutil.rmtree(v) baue_vault(v, fall.fake) # Seit der Nachbesserung Runde 2 bestimmt regelwerk_nach_edit.py seine # Vault-Wurzel ausschliesslich aus dem eigenen Skriptpfad, das Feld cwd # der Hook-Eingabe zaehlt nicht mehr. Nur eine Kopie im Wegwerf-Vault sorgt # dafuer, dass diese Wurzel auch tatsaechlich v ist statt des echten # Vaults, in dem das Original unter HOOK liegt. ziel = v / ".vault-tools" / "session-hooks" ziel.mkdir(parents=True, exist_ok=True) skript = ziel / HOOK.name shutil.copy2(HOOK, skript) umgebung = fall.umgebung(v) if fall.umgebung is not None else None code, out, err, dauer = fahre(skript, fall.stdin(v), arbeit, umgebung) kopf = f"exit={code} {dauer:.2f}s" probleme = pruefe(fall, v, code, out, dauer) if not probleme: return True, kopf text = kopf + "\n" + "\n".join(" " + p for p in probleme) if err.strip(): text += "\n stderr: " + err.strip()[:800] return False, text def ip_hook_wirft(arbeit: Path) -> tuple[bool, str]: """Fehlerpfad im Hook selbst, von aussen nicht ausloesbar: im Prozess.""" v = arbeit / "fall" if v.exists(): shutil.rmtree(v) baue_vault(v, "befund") # Kopie in v, wie in lauf_fall(): main() bestimmt die Wurzel seit Runde 2 # ausschliesslich aus dem eigenen Skriptpfad, der file_path unten zeigt # aber in v hinein - ohne die Kopie faende main() keinen Geltungsbereich. ziel = v / ".vault-tools" / "session-hooks" ziel.mkdir(parents=True, exist_ok=True) kopie = ziel / HOOK.name shutil.copy2(HOOK, kopie) spec = importlib.util.spec_from_file_location("hook_wirft", kopie) modul = importlib.util.module_from_spec(spec) spec.loader.exec_module(modul) def wirft(wurzel: Path) -> None: raise RuntimeError("Testfehler") modul.fuehre_check = wirft puffer = io.StringIO() with contextlib.redirect_stdout(puffer): code = modul.main(edit(v, str(v / "CLAUDE.md"))) erwartet = Fall("", "befund", lambda _: b"", "meldung", ["RuntimeError: Testfehler"], "regelwerk_nach_edit.py: RuntimeError") probleme = pruefe_meldung(erwartet, puffer.getvalue()) if code != 0: probleme.append(f"main gab {code} zurueck statt 0") return not probleme, "\n".join(" " + p for p in probleme) def echter_lauf() -> int: vault = HIER.parent.parent code, out, err, dauer = fahre(HOOK, edit(vault, str(vault / "CLAUDE.md")), HIER) print(f"exit={code} Laufzeit {dauer:.2f} s") if out.strip(): try: print(json.dumps(json.loads(out), ensure_ascii=False, indent=2)) except ValueError: print("FORMFEHLER, stdout ist kein JSON:") print(out) return 1 else: print("(keine Ausgabe: Regelwerk-Check ohne Befund)") if err.strip(): print("--- stderr ---") print(err) return 0 if code == 0 else 1 def main() -> int: if not HOOK.is_file(): print(f"ABBRUCH: {HOOK} fehlt") return 2 if "--echt" in sys.argv[1:]: return echter_lauf() arbeit = Path(tempfile.mkdtemp(prefix="nach-edit-faelle-")).resolve() try: print("=" * 78) print("GRUNDLINIE") print("=" * 78) for fall in GRUNDLINIE: ok, text = lauf_fall(fall, arbeit) print(f"{'BESTANDEN' if ok else 'FEHLER':10} {fall.name} {text}") if not ok: print("ABBRUCH: Die Grundlinie ist nicht gruen. Der Rest misst nichts.") return 2 print() print("=" * 78) print("ANGRIFFSLISTE") print("=" * 78) bestanden = 0 gesamt = 0 for fall in FAELLE: gesamt += 1 try: ok, text = lauf_fall(fall, arbeit) except Exception as exc: ok, text = False, f"ABSTURZ im Treiber: {type(exc).__name__}: {exc}" bestanden += int(ok) print(f"{'BESTANDEN' if ok else 'FEHLER':10} {fall.name} {text}") gesamt += 1 try: ok, text = ip_hook_wirft(arbeit) except Exception as exc: ok, text = False, f"ABSTURZ im Treiber: {type(exc).__name__}: {exc}" bestanden += int(ok) print(f"{'BESTANDEN' if ok else 'FEHLER':10} absturz | Hook selbst wirft (im Prozess) {text}") print() print(f"{bestanden} von {gesamt} Faellen bestanden.") return 0 if bestanden == gesamt else 1 finally: shutil.rmtree(arbeit, ignore_errors=True) if __name__ == "__main__": try: sys.stdout.reconfigure(encoding="utf-8") except AttributeError: pass raise SystemExit(main())