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,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)
|
||||
Reference in new issue
Block a user