Claude Code Hook Events: Wann passiert was?

Hook Events: Der richtige Zeitpunkt für Deine Automatisierung

Claude Code bietet zehn verschiedene Hook Events. Jedes Event entspricht einem bestimmten Zeitpunkt im Arbeitsablauf. Die Kunst liegt darin, den passenden Zeitpunkt für Deine Automatisierung zu wählen.

Hook Events funktionieren wie Türklingeln in einem Gebäude. Jede Klingel hängt an einer anderen Tür und läutet zu einem anderen Zeitpunkt. Wenn Du weißt, welche Tür wann geöffnet wird, kannst Du Deine Aktionen genau dort platzieren, wo sie am meisten Sinn ergeben.

Die zehn Hook Events im Überblick

Event Zeitpunkt Typische Anwendung
SessionStart Bei Sitzungsbeginn Konfiguration laden, Kontext initialisieren
UserPromptSubmit Nach Eingabe, vor Verarbeitung Eingabe validieren, Kontext anreichern
PreToolUse Vor Tool-Ausführung Backup, Validierung, Blocking
PostToolUse Nach Tool-Ausführung Logging, Qualitätsprüfung, Cleanup
Stop Bei Abbruch durch Benutzer Cleanup, Task-Status aktualisieren
SubagentStop Wenn Subagent beendet Ergebnisse zusammenführen, Logging
SessionEnd Bei Sitzungsende Statistiken speichern, Aufräumen
Notification Bei System-Benachrichtigung Weiterleitung, Logging
PermissionRequest Bei Berechtigungsanfrage Automatische Genehmigung, Logging
PreCompact Vor Kontext-Komprimierung Wichtige Infos sichern

Blocking vs. Non-Blocking Events

Ein wichtiger Unterschied: Manche Events können den Ablauf blockieren, andere nur beobachten.

Wenn Du einen Hook für PreToolUse schreibst, kann dieser die Tool-Ausführung verhindern. Bei PostToolUse ist die Ausführung bereits passiert, hier kannst Du nur noch reagieren.

Welches Event für welchen Zweck?

Faustregel: Willst Du etwas verhindern, nutze ein Pre-Event. Willst Du etwas dokumentieren oder nachbearbeiten, nutze ein Post-Event oder SessionEnd.

SessionStart: Der Startschuss

Kennst Du das Gefühl, wenn Du morgens ins Büro kommst und als Erstes den Rechner hochfährst, Kaffee holst, Deinen Kalender checkst? Genau das ist SessionStart: Der Moment, in dem alles beginnt und ich mich auf den Tag vorbereite.

Ein Sprinter kniet im Startblock. Die Muskeln gespannt, die Konzentration maximal, alle Systeme auf „bereit". Der Schuss ertönt. Und genau das ist SessionStart: Der Moment, in dem der Schuss fällt und die Arbeit beginnt.

Wann wird SessionStart ausgelöst?

SessionStart feuert exakt einmal pro Sitzung. Nicht bei jedem Prompt, nicht bei jeder Aktion. Einmal. Ganz am Anfang. Wenn Du claude in Deinem Terminal eingibst und Enter drückst, dann passiert folgendes:

  1. Claude Code startet
  2. Die Konfiguration wird geladen
  3. SessionStart feuert
  4. Alle SessionStart-Hooks werden ausgeführt
  5. Die Sitzung beginnt

Das ist der Moment, in dem ich alles vorbereiten kann, was für die gesamte Sitzung gelten soll. Einmal einrichten, dann läuft es.

Der Unterschied macht's

Warum ist das wichtig? Weil SessionStart nur einmal feuert, ist es der perfekte Ort für Dinge, die ich nicht bei jeder einzelnen Aktion wiederholen will. Datenbankverbindungen prüfen, Konfigurationen laden, den Kontext aufbauen. Alles, was „einmal am Anfang" passieren soll, gehört hierher.

Was bekomme ich im Hook zu sehen?

Die Daten, die ein SessionStart-Hook erhält, sind überschaubar aber wertvoll. Ich bekomme alles, was ich brauche, um die Sitzung einzuordnen.

JSON: SessionStart Datenstruktur

{
  "event": "SessionStart",
  "session_id": "abc123-def456-ghi789...",
  "timestamp": "2026-01-13T09:00:00Z",
  "working_directory": "/var/www/mein-projekt",
  "user": "entwickler"
}

Ich sehe also:

Mit dem working_directory kann ich zum Beispiel projektspezifische Regeln laden. Arbeite ich in /var/www/shop? Dann gelten andere Coding-Standards als in /var/www/blog.

Praxisbeispiel 1: Contract Loader

Mein Lieblings-Anwendungsfall: Projektspezifische Regeln automatisch laden. Jedes meiner Projekte hat eigene Coding-Standards, eigene Architektur-Entscheidungen, eigene „Das machen wir so"-Vereinbarungen. Die will ich nicht bei jedem Prompt neu erklären.

Also lade ich sie einmal am Anfang:

Python: Projektspezifische Contracts laden

#!/usr/bin/env python3
"""SessionStart Hook: Lädt projektspezifische Contracts"""

import json
import sys
from pathlib import Path

def lade_projekt_contracts(arbeitsverzeichnis):
    """Sucht und lädt Contracts für dieses Projekt"""
    contracts = []
    contract_pfad = Path(arbeitsverzeichnis) / '.contracts'

    if contract_pfad.exists():
        for datei in contract_pfad.glob('*.yaml'):
            contracts.append(datei.read_text())

    return contracts

def main():
    hook_daten = json.load(sys.stdin)
    arbeitsverzeichnis = hook_daten.get('working_directory', '')

    # Contracts laden
    contracts = lade_projekt_contracts(arbeitsverzeichnis)

    if contracts:
        # Als Kontext für Claude bereitstellen
        kontext = "\n\n".join(contracts)
        ergebnis = {
            "continue": True,
            "message": f"📋 {len(contracts)} Projekt-Contracts geladen:\n{kontext}"
        }
    else:
        ergebnis = {"continue": True}

    print(json.dumps(ergebnis))

if __name__ == "__main__":
    main()

Wenn ich jetzt in einem Projekt mit .contracts/-Verzeichnis arbeite, werden alle YAML-Dateien darin geladen und Claude als Kontext mitgegeben. Von Anfang an. Ohne dass ich etwas tun muss.

Praxisbeispiel 2: Sitzungsprotokollierung

Für meine Projektarbeit ist es Gold wert zu wissen: Wann habe ich an welchem Projekt gearbeitet? Wie oft? Wie lange? SessionStart ist der perfekte Ort, um das zu erfassen.

Python: Sitzungen in Datenbank protokollieren

#!/usr/bin/env python3
"""SessionStart Hook: Protokolliert Sitzungsbeginn"""

import json
import sys
import mysql.connector

def protokolliere_sitzung(hook_daten):
    """Speichert Sitzungsstart in Audit-Tabelle"""

    verbindung = mysql.connector.connect(
        host='localhost',
        database='projekt_audit',
        user='audit_user',
        password='sicheres_passwort'
    )

    cursor = verbindung.cursor()
    cursor.execute("""
        INSERT INTO sitzungen
        (sitzung_id, projekt_pfad, benutzer, gestartet_um)
        VALUES (%s, %s, %s, %s)
    """, (
        hook_daten.get('session_id'),
        hook_daten.get('working_directory'),
        hook_daten.get('user'),
        hook_daten.get('timestamp')
    ))
    verbindung.commit()
    verbindung.close()

def main():
    hook_daten = json.load(sys.stdin)

    try:
        protokolliere_sitzung(hook_daten)
        print(json.dumps({"continue": True}))
    except Exception as e:
        # Bei Fehlern trotzdem weitermachen
        print(json.dumps({
            "continue": True,
            "message": f"⚠️ Protokollierung fehlgeschlagen: {e}"
        }))

if __name__ == "__main__":
    main()

Mit dieser Protokollierung kann ich später auswerten:

Das klingt nach Spielerei, aber für die Projektabrechnung oder Zeiterfassung ist es unbezahlbar.

Praxisbeispiel 3: Infrastruktur-Check

Bevor ich mit der Arbeit beginne, will ich wissen: Sind alle Systeme bereit? Läuft die Datenbank? Ist die API erreichbar? Gibt es offene Probleme?

Bash: Infrastruktur-Status prüfen

#!/bin/bash
# SessionStart Hook: Prüft Infrastruktur-Status

STATUS=""

# Datenbank prüfen
if mysql -u root -e "SELECT 1" &>/dev/null; then
    STATUS+="✅ MySQL läuft\n"
else
    STATUS+="❌ MySQL nicht erreichbar!\n"
fi

# Redis prüfen
if redis-cli ping &>/dev/null; then
    STATUS+="✅ Redis läuft\n"
else
    STATUS+="⚠️ Redis nicht erreichbar\n"
fi

# Git-Status prüfen
if [ -d .git ]; then
    BRANCH=$(git branch --show-current 2>/dev/null)
    UNCOMMITTED=$(git status --porcelain 2>/dev/null | wc -l)
    STATUS+="📌 Git Branch: $BRANCH ($UNCOMMITTED ungespeicherte Änderungen)\n"
fi

# Ergebnis ausgeben
if [ -n "$STATUS" ]; then
    echo "{\"continue\": true, \"message\": \"🔧 Infrastruktur-Status:\n$STATUS\"}"
else
    echo '{"continue": true}'
fi

Jetzt weiß ich gleich am Anfang: Alles grün? Oder muss ich erst ein Problem lösen, bevor die eigentliche Arbeit beginnen kann?

Die Kunst des schnellen Starts

Ein wichtiger Punkt, den ich durch Erfahrung gelernt habe: SessionStart-Hooks müssen schnell sein. Wirklich schnell.

Warum? Weil sie den Sitzungsstart verzögern. Und es gibt wenig Frustrierenderes als ein System, das lange braucht, um loszulegen. Du gibst claude ein, drückst Enter, und dann... wartest Du.

Performance-Richtlinien für SessionStart

Dauer Bewertung Empfehlung
< 100ms Ideal Perfekt, nicht merkbar
100-500ms Akzeptabel Kurze Verzögerung, vertretbar
500ms-2s Grenzwertig Nur wenn wirklich nötig
> 2s Problematisch Optimieren oder auslagern

Was mache ich, wenn ein Hook länger braucht? Auslagern. Statt beim Start eine komplexe Analyse durchzuführen, speichere ich die Sitzungs-ID und lasse die Analyse im Hintergrund laufen. Die Ergebnisse kann ich dann bei Bedarf abrufen.

Der Kontext für die ganze Sitzung

Das Besondere an SessionStart: Was ich hier als message zurückgebe, steht Claude für die gesamte Sitzung zur Verfügung. Es ist wie ein Briefing am Morgen: „Das sind die Regeln heute, das ist der Status, das solltest Du wissen."

Python: Umfassenden Kontext bereitstellen

def erstelle_sitzungskontext(arbeitsverzeichnis):
    """Baut einen umfassenden Kontext für die Sitzung"""

    kontext_teile = []

    # Projekt-Typ erkennen
    if (Path(arbeitsverzeichnis) / 'composer.json').exists():
        kontext_teile.append("📦 PHP-Projekt (Composer)")
        kontext_teile.append("Nutze PSR-4 Autoloading")
        kontext_teile.append("Typisiere alle Methoden-Parameter")

    elif (Path(arbeitsverzeichnis) / 'package.json').exists():
        kontext_teile.append("📦 Node.js-Projekt")
        kontext_teile.append("Nutze ES Modules (import/export)")
        kontext_teile.append("Typisiere mit TypeScript wenn vorhanden")

    # Offene Tickets laden
    tickets = lade_offene_tickets(arbeitsverzeichnis)
    if tickets:
        kontext_teile.append(f"🎫 {len(tickets)} offene Tickets")
        for ticket in tickets[:3]:
            kontext_teile.append(f"  - {ticket}")

    return "\n".join(kontext_teile)

Mit diesem Ansatz weiß Claude von Anfang an:

Das spart enorm viele Erklärungen und führt zu besseren, konsistenteren Ergebnissen.

Was ich nicht in SessionStart mache

Genauso wichtig wie zu wissen, was SessionStart kann, ist zu verstehen, was dort nicht hingehört:

Das Briefing-Prinzip

Denk an SessionStart wie an ein kurzes Briefing vor einem Meeting. Du sagst nicht alles, was Du weißt. Du sagst das Wichtigste, das Relevanteste, das, was für diese Sitzung gilt. Kurz, prägnant, auf den Punkt.

Zusammenspiel mit anderen Events

SessionStart ist der Anfang einer Kette. Es definiert den Rahmen, in dem alle anderen Events arbeiten. Die Beziehung sieht so aus:

Event Beziehung zu SessionStart
UserPromptSubmit Nutzt den Kontext, den SessionStart bereitgestellt hat
PreToolUse Kann auf SessionStart-Konfiguration zugreifen
PostToolUse Protokolliert mit der Session-ID von SessionStart
SessionEnd Schließt, was SessionStart begonnen hat

SessionStart und SessionEnd bilden zusammen die Klammer um eine Arbeitssitzung. Der Startschuss und der Feierabend. Alles dazwischen passiert im Kontext, den SessionStart gesetzt hat.

Zusammenfassung

SessionStart ist der Moment, in dem alles beginnt. Der Startschuss. Der erste Atemzug einer neuen Arbeitssitzung. Hier bereite ich den Boden:

Dabei gilt: Schnell bleiben. Der Benutzer will arbeiten, nicht warten. Alles, was länger dauert, gehört in den Hintergrund oder in einen späteren Hook.

SessionStart ist kein Ort für komplexe Logik. Es ist ein Ort für klares, schnelles Setup. Der Sprinter im Startblock. Der Schuss fällt. Los geht's.

Der erste Eindruck zählt

SessionStart ist das Erste, was passiert. Wenn es schiefgeht, startet die Sitzung nicht. Wenn es langsam ist, nervt es. Wenn es gut gemacht ist, bemerkt es niemand. Und genau das ist das Ziel: unsichtbar gut arbeiten.

UserPromptSubmit: Die Eingabe abfangen

Kennst Du das Gefühl, wenn Du einen Brief einwirfst und ihn im letzten Moment noch einmal herausziehen könntest? Genau das ist UserPromptSubmit: Der Moment zwischen Absenden und Verarbeiten, in dem ich die Eingabe noch abfangen, prüfen und anreichern kann.

Es ist wie bei einem aufmerksamen Assistenten, der direkt neben dem Briefkasten steht. Du wirfst den Brief ein, aber bevor er in den Schlund fällt, fängt der Assistent ihn ab. Er schaut kurz drauf: „Ist der Empfänger korrekt? Fehlt vielleicht noch etwas? Soll ich einen Vermerk hinzufügen?" Und erst dann geht der Brief weiter auf seine Reise.

Wann wird UserPromptSubmit ausgelöst?

UserPromptSubmit feuert jedes Mal, wenn Du als Benutzer eine Nachricht an Claude sendest. Der Ablauf sieht so aus:

  1. Du tippst Deine Nachricht
  2. Du drückst Enter
  3. UserPromptSubmit feuert
  4. Alle UserPromptSubmit-Hooks werden ausgeführt
  5. Claude erhält die (möglicherweise angereicherte) Nachricht
  6. Claude beginnt mit der Verarbeitung

Das ist der entscheidende Moment zwischen „Benutzer hat etwas gesagt" und „Claude denkt darüber nach". Hier kann ich eingreifen.

Der Unterschied zu SessionStart

SessionStart feuert einmal am Anfang. UserPromptSubmit feuert bei jeder einzelnen Nachricht. Das sind unterschiedliche Zeitpunkte für unterschiedliche Zwecke. SessionStart für die Grundkonfiguration, UserPromptSubmit für die laufende Anreicherung.

Was bekomme ich im Hook zu sehen?

Das Herzstück von UserPromptSubmit ist der Prompt selbst. Ich sehe genau, was der Benutzer geschrieben hat, und kann darauf reagieren.

JSON: UserPromptSubmit Datenstruktur

{
  "event": "UserPromptSubmit",
  "session_id": "abc123-def456-ghi789...",
  "prompt": "Erstelle eine neue PHP-Klasse für die Benutzerauthentifizierung",
  "timestamp": "2026-01-13T10:15:00Z"
}

Ich sehe also:

Mit dem prompt kann ich erkennen, worum es geht. Will der Benutzer Code schreiben? Eine Frage stellen? Etwas debuggen? Je nach Inhalt kann ich unterschiedlich reagieren.

Die Blocking-Fähigkeit

UserPromptSubmit ist eines der wenigen Events, die blockieren können. Das bedeutet: Ich kann die Verarbeitung komplett stoppen, bevor sie überhaupt beginnt.

Das klingt drastisch, und das ist es auch. Aber manchmal ist es nötig. Wenn jemand versucht, gefährliche Befehle auszuführen oder gegen Projektregeln zu verstoßen, kann ich eingreifen, bevor Schaden entsteht.

Python: Eingabe blockieren

# So blockiere ich eine Eingabe:
ergebnis = {
    "hookSpecificOutput": {
        "hookEventName": "UserPromptSubmit",
        "permissionDecision": "deny",
        "permissionDecisionReason": "Diese Aktion ist nicht erlaubt."
    }
}
print(json.dumps(ergebnis))

Wenn ich "permissionDecision": "deny" zurückgebe, wird die Nachricht nicht an Claude weitergeleitet. Der Benutzer sieht stattdessen die permissionDecisionReason.

Praxisbeispiel 1: Intelligente Kontext-Anreicherung

Mein häufigster Anwendungsfall: Den Prompt mit relevantem Kontext anreichern. Wenn der Benutzer etwas über eine bestimmte Komponente fragt, lade ich automatisch die Dokumentation zu dieser Komponente.

Python: Prompt mit Kontext anreichern

#!/usr/bin/env python3
"""UserPromptSubmit Hook: Reichert Prompts mit Kontext an"""

import json
import sys
import re

def finde_erwaehnte_komponenten(prompt):
    """Erkennt erwähnte Komponenten im Prompt"""
    komponenten = []

    # Bekannte Komponenten-Muster
    muster = {
        r'auth': 'AuthService',
        r'user|benutzer': 'UserService',
        r'payment|zahlung': 'PaymentService',
        r'mail|email': 'MailService',
    }

    for pattern, komponente in muster.items():
        if re.search(pattern, prompt.lower()):
            komponenten.append(komponente)

    return komponenten

def lade_dokumentation(komponenten):
    """Lädt Dokumentation für die gefundenen Komponenten"""
    doku_teile = []

    for komponente in komponenten:
        doku_pfad = Path(f'.docs/{komponente}.md')
        if doku_pfad.exists():
            doku_teile.append(f"## {komponente}\n{doku_pfad.read_text()}")

    return "\n\n".join(doku_teile)

def main():
    hook_daten = json.load(sys.stdin)
    prompt = hook_daten.get('prompt', '')

    # Erwähnte Komponenten finden
    komponenten = finde_erwaehnte_komponenten(prompt)

    if komponenten:
        # Dokumentation laden und als Kontext hinzufügen
        doku = lade_dokumentation(komponenten)
        ergebnis = {
            "continue": True,
            "message": f"📚 Relevante Dokumentation:\n{doku}"
        }
    else:
        ergebnis = {"continue": True}

    print(json.dumps(ergebnis))

if __name__ == "__main__":
    main()

Wenn der Benutzer jetzt fragt „Wie funktioniert die Authentifizierung?", erkennt mein Hook das Wort „auth" und lädt automatisch die AuthService-Dokumentation. Claude hat dann sofort den nötigen Kontext, ohne dass der Benutzer extra erklären muss, wo die Doku liegt.

Praxisbeispiel 2: Prompt-Protokollierung

Für manche Projekte ist es wichtig nachzuvollziehen, welche Anfragen gestellt wurden. Nicht aus Kontrollzwang, sondern um zu verstehen: Was brauchen die Benutzer? Welche Fragen kommen häufig? Wo fehlt Dokumentation?

Python: Prompts in Datenbank protokollieren

#!/usr/bin/env python3
"""UserPromptSubmit Hook: Protokolliert alle Prompts"""

import json
import sys
import hashlib
import mysql.connector

def protokolliere_prompt(hook_daten):
    """Speichert Prompt in Audit-Tabelle"""

    prompt = hook_daten.get('prompt', '')

    # Für Datenschutz: Prompt hashen statt speichern
    prompt_hash = hashlib.sha256(prompt.encode()).hexdigest()[:16]

    # Kategorie erkennen
    kategorie = erkenne_kategorie(prompt)

    verbindung = mysql.connector.connect(
        host='localhost',
        database='projekt_audit',
        user='audit_user',
        password='sicheres_passwort'
    )

    cursor = verbindung.cursor()
    cursor.execute("""
        INSERT INTO prompt_log
        (sitzung_id, prompt_hash, kategorie, laenge, zeitstempel)
        VALUES (%s, %s, %s, %s, %s)
    """, (
        hook_daten.get('session_id'),
        prompt_hash,
        kategorie,
        len(prompt),
        hook_daten.get('timestamp')
    ))
    verbindung.commit()
    verbindung.close()

def erkenne_kategorie(prompt):
    """Kategorisiert den Prompt"""
    prompt_lower = prompt.lower()

    if any(w in prompt_lower for w in ['erstelle', 'schreibe', 'implementiere']):
        return 'erstellung'
    elif any(w in prompt_lower for w in ['fix', 'fehler', 'bug', 'repariere']):
        return 'debugging'
    elif any(w in prompt_lower for w in ['erkläre', 'was ist', 'wie funktioniert']):
        return 'frage'
    else:
        return 'sonstiges'

def main():
    hook_daten = json.load(sys.stdin)

    try:
        protokolliere_prompt(hook_daten)
    except Exception:
        pass  # Protokollierung darf nie blockieren

    print(json.dumps({"continue": True}))

if __name__ == "__main__":
    main()

Mit dieser Protokollierung kann ich später analysieren:

Das hilft mir, die Dokumentation zu verbessern und häufige Probleme proaktiv anzugehen.

Praxisbeispiel 3: Gefährliche Anfragen blockieren

Manchmal muss ich eingreifen. Wenn jemand versucht, etwas Gefährliches zu tun, oder wenn die Anfrage gegen Projektregeln verstößt, blockiere ich sie, bevor sie überhaupt bei Claude ankommt.

Python: Gefährliche Anfragen blockieren

#!/usr/bin/env python3
"""UserPromptSubmit Hook: Blockiert gefährliche Anfragen"""

import json
import sys
import re

# Gefährliche Muster
VERBOTENE_MUSTER = [
    (r'rm\s+-rf\s+/', "Rekursives Löschen von / ist verboten"),
    (r'drop\s+database', "Datenbank-Löschung ist verboten"),
    (r'truncate\s+table', "Tabellen-Truncate ist verboten"),
    (r'chmod\s+777', "chmod 777 ist ein Sicherheitsrisiko"),
    (r'--no-verify', "Git-Hooks dürfen nicht übersprungen werden"),
]

def pruefe_prompt(prompt):
    """Prüft Prompt auf verbotene Muster"""
    prompt_lower = prompt.lower()

    for muster, grund in VERBOTENE_MUSTER:
        if re.search(muster, prompt_lower):
            return grund

    return None

def main():
    hook_daten = json.load(sys.stdin)
    prompt = hook_daten.get('prompt', '')

    # Auf gefährliche Muster prüfen
    blockier_grund = pruefe_prompt(prompt)

    if blockier_grund:
        ergebnis = {
            "hookSpecificOutput": {
                "hookEventName": "UserPromptSubmit",
                "permissionDecision": "deny",
                "permissionDecisionReason": f"⛔ Blockiert: {blockier_grund}"
            }
        }
    else:
        ergebnis = {"continue": True}

    print(json.dumps(ergebnis))

if __name__ == "__main__":
    main()

Dieser Hook fängt potentiell gefährliche Anfragen ab, bevor Claude sie überhaupt sieht. Das ist eine zusätzliche Sicherheitsschicht, die unabhängig von Claudes eigenem Sicherheitsbewusstsein funktioniert.

Die Kunst der Anreicherung

Das Mächtigste an UserPromptSubmit ist die Möglichkeit, den Kontext anzureichern. Nicht den Prompt selbst verändern, aber Informationen hinzufügen, die Claude helfen, bessere Antworten zu geben.

Was kann ich als Kontext hinzufügen?

Kontext-Typ Beispiel Nutzen
Dokumentation API-Referenz, Style-Guides Claude kennt die Regeln
Code-Kontext Relevante Klassen, Interfaces Claude versteht die Architektur
Projektstatus Offene Tickets, Sprint-Ziele Claude kennt die Prioritäten
Historische Daten Frühere Entscheidungen Claude wiederholt keine Fehler

Je mehr relevanten Kontext Claude hat, desto bessere Antworten bekomme ich. UserPromptSubmit ist der ideale Ort, um diesen Kontext automatisch bereitzustellen.

Der Unterschied zwischen Blockieren und Anreichern

Es gibt zwei grundsätzlich verschiedene Reaktionen in UserPromptSubmit:

Aspekt Blockieren Anreichern
Wann Bei Verstößen, Gefahren Bei jeder Anfrage
Wie permissionDecision: deny message: "Kontext..."
Ergebnis Anfrage wird abgelehnt Anfrage wird bereichert
Häufigkeit Selten (Ausnahme) Häufig (Standard)

In der Praxis blockiere ich selten. Die meiste Zeit reichere ich an. Blockieren ist die Notbremse, Anreichern ist der Normalfall.

Die goldene Regel

Blockiere nur, wenn es wirklich nötig ist. Ansonsten: Bereichere. Gib Claude mehr Kontext, mehr Informationen, mehr Hilfestellung. Das führt zu besseren Ergebnissen als restriktives Blockieren.

Performance-Überlegungen

Anders als SessionStart (einmal pro Sitzung) feuert UserPromptSubmit bei jeder einzelnen Nachricht. Das bedeutet: Meine Hooks müssen schnell sein. Wirklich schnell.

Ein guter UserPromptSubmit-Hook sollte in unter 50 Millisekunden fertig sein. Alles darüber verzögert jede einzelne Interaktion spürbar.

Zusammenspiel mit PreToolUse

UserPromptSubmit und PreToolUse ergänzen sich. Sie arbeiten auf verschiedenen Ebenen:

Event Prüft Beispiel
UserPromptSubmit Die Absicht „Lösche die Datenbank" → Blockiert
PreToolUse Die Aktion Bash: rm -rf / → Blockiert

UserPromptSubmit fängt die Absicht ab. PreToolUse fängt die konkrete Aktion ab. Zusammen bilden sie ein zweistufiges Sicherheitsnetz.

Zusammenfassung

UserPromptSubmit ist der Moment zwischen Absenden und Verarbeiten. Der aufmerksame Assistent am Briefkasten, der jeden Brief noch einmal kurz prüft. Hier kann ich:

Dabei gilt: Schnell bleiben. UserPromptSubmit feuert bei jeder Nachricht. Jede Verzögerung hier spürt der Benutzer bei jeder einzelnen Interaktion.

Das Mächtigste ist die Anreicherung. Je mehr relevanten Kontext Claude hat, desto bessere Antworten bekomme ich. UserPromptSubmit ist der ideale Ort, um diesen Kontext automatisch und unsichtbar bereitzustellen.

Der unsichtbare Helfer

Der beste UserPromptSubmit-Hook ist einer, den der Benutzer nicht bemerkt. Er arbeitet im Hintergrund, reichert den Kontext an, protokolliert bei Bedarf, und nur im Notfall macht er sich durch eine Blockierung bemerkbar. Unsichtbar gut. Genau wie ein guter Assistent.

PreToolUse: Der Wächter vor der Aktion

Kennst Du den Sicherheitsmann am Eingang, der jeden Besucher kurz mustert, bevor er ihn durchlässt? Genau das ist PreToolUse: Der Wächter, der jede Aktion prüft, bevor sie ausgeführt wird. Und der im Zweifelsfall sagt: „Stopp, hier kommst Du nicht durch."

PreToolUse ist das mächtigste aller Hook Events. Hier kann ich nicht nur beobachten, hier kann ich eingreifen. Jede Dateiänderung, jeder Befehl, jede Aktion muss an diesem Wächter vorbei. Und ich entscheide: Durchlassen oder blockieren.

Wann wird PreToolUse ausgelöst?

PreToolUse feuert jedes Mal, bevor Claude ein Werkzeug verwendet. Der Ablauf sieht so aus:

  1. Claude entscheidet, ein Werkzeug zu nutzen
  2. Claude bereitet die Parameter vor
  3. PreToolUse feuert
  4. Alle PreToolUse-Hooks werden ausgeführt
  5. Wenn alle Hooks OK geben: Werkzeug wird ausgeführt
  6. Wenn ein Hook blockiert: Werkzeug wird NICHT ausgeführt

Das ist der kritische Moment. Die Entscheidung fällt hier. Danach ist es zu spät.

Die Macht des Wächters

PreToolUse ist der einzige Zeitpunkt, an dem ich eine Werkzeug-Ausführung tatsächlich verhindern kann. Bei PostToolUse ist es bereits passiert. Hier ist der letzte Moment, um „Nein" zu sagen.

Welche Werkzeuge gibt es?

Claude Code nutzt verschiedene Werkzeuge für verschiedene Aufgaben. PreToolUse sieht jedes einzelne davon:

Werkzeug Funktion Typische PreToolUse-Prüfung
Edit Datei bearbeiten Backup erstellen, Syntax prüfen
Write Neue Datei erstellen Pfad validieren, Überschreiben verhindern
Bash Befehl ausführen Gefährliche Befehle blockieren
Read Datei lesen Zugriff auf sensible Dateien prüfen
Glob Dateien suchen Meist durchlassen
Grep In Dateien suchen Meist durchlassen
Task Subagent starten Ressourcen-Limits prüfen

Für jedes dieser Werkzeuge kann ich entscheiden: Durchlassen, mit Hinweis durchlassen, oder blockieren.

Was bekomme ich im Hook zu sehen?

PreToolUse liefert mir alle Informationen über die geplante Aktion. Ich sehe nicht nur, welches Werkzeug verwendet werden soll, sondern auch alle Parameter.

JSON: PreToolUse Datenstruktur (Edit)

{
  "event": "PreToolUse",
  "session_id": "abc123-def456-ghi789...",
  "tool_name": "Edit",
  "tool_input": {
    "file_path": "/var/www/projekt/src/Service.php",
    "old_string": "function alte_methode()",
    "new_string": "function neue_methode()"
  },
  "timestamp": "2026-01-13T10:30:00Z"
}

Bei einem Bash-Befehl sehe ich den kompletten Befehl:

JSON: PreToolUse Datenstruktur (Bash)

{
  "event": "PreToolUse",
  "session_id": "abc123-def456-ghi789...",
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm install lodash",
    "description": "Install lodash package"
  },
  "timestamp": "2026-01-13T10:31:00Z"
}

Mit diesen Informationen kann ich präzise entscheiden, ob die Aktion erlaubt ist oder nicht.

Tool-Spezifische Matcher

Eine Besonderheit von PreToolUse: Ich kann Hooks auf bestimmte Werkzeuge beschränken. Das spart Ressourcen und macht den Code übersichtlicher.

JSON: Hook-Konfiguration mit Matcher

{
  "name": "file_backup",
  "event": "PreToolUse",
  "enabled": true,
  "matcher": "^(Edit|Write)$",
  "command": "python3 /hooks/backup_before_edit.py"
}

Dieser Hook feuert nur bei Edit und Write, nicht bei Read, Bash oder anderen Werkzeugen. Der Matcher ist ein regulärer Ausdruck, der auf den Werkzeugnamen angewendet wird.

Praxisbeispiel 1: Automatisches Datei-Backup

Mein wichtigster PreToolUse-Hook: Vor jeder Dateiänderung ein Backup erstellen. Das gibt mir die Sicherheit, jederzeit zurückgehen zu können.

Python: Automatisches Backup vor Änderungen

#!/usr/bin/env python3
"""PreToolUse Hook: Erstellt Backup vor Dateiänderungen"""

import json
import sys
import shutil
from pathlib import Path
from datetime import datetime

BACKUP_VERZEICHNIS = Path('/var/backups/claude-code')

def erstelle_backup(dateipfad):
    """Kopiert die Datei ins Backup-Verzeichnis"""
    datei = Path(dateipfad)

    if not datei.exists():
        return None  # Neue Datei, kein Backup nötig

    # Backup-Pfad mit Zeitstempel
    zeitstempel = datetime.now().strftime('%Y%m%d_%H%M%S')
    backup_name = f"{datei.stem}_{zeitstempel}{datei.suffix}"
    backup_pfad = BACKUP_VERZEICHNIS / backup_name

    # Verzeichnis erstellen falls nötig
    BACKUP_VERZEICHNIS.mkdir(parents=True, exist_ok=True)

    # Kopieren
    shutil.copy2(datei, backup_pfad)

    return backup_pfad

def main():
    hook_daten = json.load(sys.stdin)

    werkzeug = hook_daten.get('tool_name')
    eingabe = hook_daten.get('tool_input', {})

    # Nur bei Dateiänderungen
    if werkzeug not in ['Edit', 'Write']:
        print(json.dumps({"continue": True}))
        return

    dateipfad = eingabe.get('file_path', '')

    try:
        backup_pfad = erstelle_backup(dateipfad)
        if backup_pfad:
            print(json.dumps({
                "continue": True,
                "message": f"💾 Backup erstellt: {backup_pfad}"
            }))
        else:
            print(json.dumps({"continue": True}))
    except Exception as e:
        # Bei Backup-Fehlern trotzdem weitermachen, aber warnen
        print(json.dumps({
            "continue": True,
            "message": f"⚠️ Backup fehlgeschlagen: {e}"
        }))

if __name__ == "__main__":
    main()

Mit diesem Hook habe ich für jede geänderte Datei eine Sicherungskopie. Wenn etwas schiefgeht, kann ich jederzeit zurück.

Praxisbeispiel 2: Gefährliche Befehle blockieren

Das zweite Standbein meiner Sicherheitsstrategie: Bestimmte Befehle komplett verbieten. Es gibt Dinge, die sollen einfach nicht passieren.

Python: Gefährliche Befehle blockieren

#!/usr/bin/env python3
"""PreToolUse Hook: Blockiert gefährliche Bash-Befehle"""

import json
import sys
import re

# Verbotene Muster mit Erklärungen
VERBOTENE_MUSTER = [
    (r'\brm\s+-rf\s+/', "Rekursives Löschen von / ist verboten"),
    (r'\bcurl\b', "curl ist verboten. Nutze: http get URL"),
    (r'\bwget\b', "wget ist verboten. Nutze: download URL"),
    (r'\bmysql\s+-e', "Direkter MySQL-Zugriff verboten. Nutze: db_sql"),
    (r'--no-verify', "Git-Hooks dürfen nicht übersprungen werden"),
    (r'--force\s+push', "Force-Push ist verboten"),
    (r'\bchmod\s+777\b', "chmod 777 ist ein Sicherheitsrisiko"),
]

# Erlaubte Ausnahmen
ERLAUBTE_MUSTER = [
    r'curl\s+--version',  # Versionsabfrage OK
]

def pruefe_befehl(befehl):
    """Prüft Befehl auf verbotene Muster"""

    # Erst Ausnahmen prüfen
    for erlaubt in ERLAUBTE_MUSTER:
        if re.search(erlaubt, befehl, re.IGNORECASE):
            return None

    # Dann verbotene Muster prüfen
    for muster, grund in VERBOTENE_MUSTER:
        if re.search(muster, befehl, re.IGNORECASE):
            return grund

    return None

def main():
    hook_daten = json.load(sys.stdin)

    # Nur bei Bash-Befehlen prüfen
    if hook_daten.get('tool_name') != 'Bash':
        print(json.dumps({"continue": True}))
        return

    befehl = hook_daten.get('tool_input', {}).get('command', '')
    blockier_grund = pruefe_befehl(befehl)

    if blockier_grund:
        ergebnis = {
            "hookSpecificOutput": {
                "hookEventName": "PreToolUse",
                "permissionDecision": "deny",
                "permissionDecisionReason": f"⛔ {blockier_grund}"
            }
        }
    else:
        ergebnis = {"continue": True}

    print(json.dumps(ergebnis))

if __name__ == "__main__":
    main()

Dieser Hook macht mehrere Dinge:

Praxisbeispiel 3: Dokumentation vor Bearbeitung anzeigen

Ein eleganter Anwendungsfall: Bevor Claude eine Datei bearbeitet, zeige ich ihm die relevante Dokumentation. So kennt er die Regeln, bevor er anfängt.

Python: Dokumentation vor Bearbeitung laden

#!/usr/bin/env python3
"""PreToolUse Hook: Zeigt relevante Dokumentation vor Bearbeitung"""

import json
import sys
from pathlib import Path

def finde_dokumentation(dateipfad):
    """Sucht Dokumentation für die Datei oder ihr Verzeichnis"""
    datei = Path(dateipfad)
    doku_teile = []

    # Projekt-weite CLAUDE.md
    projekt_root = datei.parent
    while projekt_root != projekt_root.parent:
        claude_md = projekt_root / 'CLAUDE.md'
        if claude_md.exists():
            doku_teile.append(f"📋 Projekt-Regeln:\n{claude_md.read_text()[:500]}...")
            break
        projekt_root = projekt_root.parent

    # Verzeichnis-spezifische Dokumentation
    verzeichnis_doku = datei.parent / 'README.md'
    if verzeichnis_doku.exists():
        doku_teile.append(f"📁 Verzeichnis-Info:\n{verzeichnis_doku.read_text()[:300]}...")

    # Datei-spezifischer Contract
    contract = datei.with_suffix('.contract.yaml')
    if contract.exists():
        doku_teile.append(f"📄 Datei-Contract:\n{contract.read_text()}")

    return "\n\n".join(doku_teile) if doku_teile else None

def main():
    hook_daten = json.load(sys.stdin)

    # Nur bei Datei-Bearbeitung
    if hook_daten.get('tool_name') not in ['Edit', 'Write']:
        print(json.dumps({"continue": True}))
        return

    dateipfad = hook_daten.get('tool_input', {}).get('file_path', '')
    doku = finde_dokumentation(dateipfad)

    if doku:
        print(json.dumps({
            "continue": True,
            "message": f"📚 Relevante Dokumentation:\n{doku}"
        }))
    else:
        print(json.dumps({"continue": True}))

if __name__ == "__main__":
    main()

Mit diesem Hook weiß Claude automatisch, welche Regeln für diese Datei gelten. Er muss nicht erst nachfragen, und ich muss nicht alles bei jedem Prompt wiederholen.

Die drei Reaktionsmöglichkeiten

In PreToolUse habe ich drei grundlegende Möglichkeiten zu reagieren:

Was kann PreToolUse tun?

Reaktion Wie Wann nutzen
Durchlassen {"continue": true} Aktion ist OK
Mit Hinweis durchlassen {"continue": true, "message": "..."} OK, aber Claude sollte etwas beachten
Blockieren {"hookSpecificOutput": {...}} Aktion ist verboten

In der Praxis blockiere ich selten. Die meisten Hooks lassen durch, manchmal mit einem Hinweis. Blockieren ist die Ausnahme, nicht die Regel.

Performance ist kritisch

PreToolUse feuert bei jeder einzelnen Werkzeug-Nutzung. Das können viele Ereignisse in kurzer Zeit sein. Wenn Claude zehn Dateien liest, feuert PreToolUse zehn Mal. Meine Hooks müssen schnell sein.

Die 100-Millisekunden-Regel

Ein guter PreToolUse-Hook sollte in unter 100 Millisekunden fertig sein. Alles darüber verlangsamt jede einzelne Werkzeug-Nutzung spürbar. Bei einer Session mit hunderten von Tool-Aufrufen summiert sich das schnell.

Das Zusammenspiel der Wächter

PreToolUse arbeitet nicht allein. Es ist Teil eines mehrschichtigen Sicherheitssystems:

Schicht Event Prüft
1 UserPromptSubmit Die Absicht des Benutzers
2 PreToolUse Die konkrete Aktion
3 PostToolUse Das Ergebnis der Aktion

Wenn UserPromptSubmit die erste Sicherheitskontrolle ist (Absichten prüfen), dann ist PreToolUse die zweite (Aktionen prüfen), und PostToolUse die dritte (Ergebnisse prüfen). Zusammen bilden sie ein robustes Sicherheitsnetz.

Typische Hook-Kombinationen

In meinem Setup nutze ich mehrere PreToolUse-Hooks gleichzeitig. Jeder hat seine spezifische Aufgabe:

JSON: Mehrere PreToolUse-Hooks

[
  {
    "name": "backup_vor_aenderung",
    "event": "PreToolUse",
    "matcher": "^(Edit|Write)$",
    "command": "python3 /hooks/backup.py"
  },
  {
    "name": "gefaehrliche_befehle",
    "event": "PreToolUse",
    "matcher": "^Bash$",
    "command": "python3 /hooks/bash_guard.py"
  },
  {
    "name": "dokumentation_laden",
    "event": "PreToolUse",
    "matcher": "^(Edit|Write)$",
    "command": "python3 /hooks/doku_awareness.py"
  }
]

Die Hooks werden der Reihe nach ausgeführt. Wenn einer blockiert, werden die nachfolgenden nicht mehr aufgerufen.

Zusammenfassung

PreToolUse ist der Wächter vor der Aktion. Der letzte Moment, um „Nein" zu sagen. Hier kann ich:

Die Macht von PreToolUse liegt in der Kombination: Ich kann die Aktion sehen, bevor sie passiert, und ich kann sie stoppen. Das ist einzigartig unter allen Hook Events.

Dabei gilt: Mit Macht kommt Verantwortung. Ein fehlerhafter PreToolUse-Hook kann die gesamte Arbeit blockieren. Ein langsamer Hook verlangsamt jede Aktion. Deshalb: Gründlich testen, schnell halten, und nur blockieren, wenn es wirklich nötig ist.

Der gute Wächter

Ein guter Wächter lässt die meisten Besucher durch. Er grüßt freundlich, manchmal gibt er einen Hinweis, und nur selten muss er „Stopp" sagen. So sollte auch PreToolUse sein: Unterstützend im Normalfall, streng nur wenn nötig. Der beste Hook ist einer, dessen Blockierungen man nie sieht, weil sie nie nötig werden.

PostToolUse: Die Nachbereitung

Kennst Du das Gefühl, wenn Du gerade etwas erledigt hast und dann noch einmal drüberschaust, ob auch wirklich alles passt? Genau das ist PostToolUse: Der Moment direkt nach der Aktion, in dem ich prüfen, protokollieren und bei Bedarf Feedback geben kann.

Wenn PreToolUse der Türsteher ist, dann ist PostToolUse der Qualitätsprüfer am Ende des Fließbands. Die Aktion ist bereits passiert, das Werkzeug hat seine Arbeit getan. Jetzt geht es darum, das Ergebnis zu begutachten.

Wann wird PostToolUse ausgelöst?

PostToolUse feuert unmittelbar nach Abschluss einer Werkzeug-Ausführung. Das bedeutet:

Das ist ein wichtiger Unterschied zu PreToolUse: Dort konnte ich noch eingreifen und sagen „Stopp, das machen wir nicht!" Bei PostToolUse ist es dafür zu spät. Die Aktion ist passiert. Aber ich kann reagieren, dokumentieren und Feedback geben.

Der richtige Zeitpunkt

PostToolUse ist wie der Moment, in dem der Koch das Gericht fertig angerichtet hat und der Küchenchef einen letzten Blick darauf wirft. Er kann das Gericht nicht mehr „unkochen", aber er kann prüfen, ob alles stimmt, bevor es zum Gast geht. Und er kann sich Notizen machen für das nächste Mal.

Was bekomme ich im Hook zu sehen?

Das Schöne an PostToolUse: Ich bekomme nicht nur die Eingabe, sondern auch das Ergebnis. Das ist wie ein vollständiges Protokoll der gerade abgeschlossenen Aktion.

JSON: PostToolUse Datenstruktur

{
  "event": "PostToolUse",
  "session_id": "abc123...",
  "tool_name": "Edit",
  "tool_input": {
    "file_path": "/var/www/projekt/src/Service.php",
    "old_string": "function alte_methode()",
    "new_string": "function neue_methode()"
  },
  "tool_result": {
    "success": true,
    "message": "File updated successfully"
  },
  "timestamp": "2026-01-13T12:10:05Z"
}

Ich sehe also:

Mit diesen Informationen kann ich eine Menge anfangen.

Warum nicht einfach blockieren?

Du fragst Dich vielleicht: „Warum kann ich bei PostToolUse nicht auch blockieren wie bei PreToolUse?"

Die Antwort ist einfach: Die Aktion ist bereits passiert. Die Datei wurde geändert, der Befehl wurde ausgeführt. Es gibt nichts mehr zu blockieren. Das wäre, als würdest Du nach dem Versenden einer E-Mail sagen: „Eigentlich wollte ich die gar nicht abschicken." Zu spät.

Aber: Du kannst Feedback geben. Du kannst sagen: „Achtung, da ist etwas schiefgelaufen!" Und Claude wird dieses Feedback bei der nächsten Aktion berücksichtigen.

Praxisbeispiel 1: Automatische Syntaxprüfung

Mein häufigstes Einsatzgebiet für PostToolUse: Syntaxprüfung direkt nach einer Dateiänderung. Das funktioniert so:

  1. Claude bearbeitet eine PHP-Datei
  2. PostToolUse feuert
  3. Mein Hook führt php -l dateiname.php aus
  4. Bei einem Syntaxfehler gebe ich eine Warnung zurück
  5. Claude sieht die Warnung und korrigiert den Fehler

Das ist wie ein aufmerksamer Korrekturleser, der nach jedem Absatz kurz drüberschaut.

Python: Syntax-Prüfung nach Bearbeitung

#!/usr/bin/env python3
"""PostToolUse Hook: Prüft Code-Syntax nach Änderungen"""

import json
import sys
import subprocess

def pruefe_php_syntax(dateipfad):
    """Führt php -l aus und gibt Fehler zurück"""
    ergebnis = subprocess.run(
        ['php', '-l', dateipfad],
        capture_output=True, text=True
    )
    if ergebnis.returncode != 0:
        return ergebnis.stderr
    return None

def main():
    hook_daten = json.load(sys.stdin)

    # Nur bei Dateiänderungen reagieren
    if hook_daten.get('tool_name') not in ['Edit', 'Write']:
        print(json.dumps({"continue": True}))
        return

    dateipfad = hook_daten.get('tool_input', {}).get('file_path', '')

    # PHP-Dateien prüfen
    if dateipfad.endswith('.php'):
        fehler = pruefe_php_syntax(dateipfad)
        if fehler:
            print(json.dumps({
                "continue": True,
                "message": f"⚠️ PHP-Syntaxfehler gefunden:\n{fehler}"
            }))
            return

    print(json.dumps({"continue": True}))

if __name__ == "__main__":
    main()

Der Schlüssel ist die message im Rückgabewert. Diese Nachricht wird Claude angezeigt und fließt in den Kontext ein. Claude sieht dann: „Aha, da ist ein Syntaxfehler. Den sollte ich beheben."

Praxisbeispiel 2: Automatische Berechtigungskorrektur

Ein Problem, das mich früher regelmäßig erwischt hat: Claude erstellt eine Datei, aber der Webserver kann sie nicht lesen, weil die Berechtigungen falsch sind. Jetzt löse ich das automatisch:

Bash: Berechtigungen automatisch korrigieren

#!/bin/bash
# PostToolUse Hook: Setzt Datei-Berechtigungen

# Hook-Daten einlesen
HOOK_DATEN=$(cat)
WERKZEUG=$(echo "$HOOK_DATEN" | jq -r '.tool_name')
DATEIPFAD=$(echo "$HOOK_DATEN" | jq -r '.tool_input.file_path // empty')

# Nur bei Dateioperationen
if [[ "$WERKZEUG" == "Edit" || "$WERKZEUG" == "Write" ]]; then
    if [[ -n "$DATEIPFAD" && -f "$DATEIPFAD" ]]; then
        # Besitzer auf www-data setzen
        chown www-data:www-data "$DATEIPFAD"
        # Lesbare Berechtigungen setzen
        chmod 644 "$DATEIPFAD"
    fi
fi

# Immer weitermachen
echo '{"continue": true}'

Dieser Hook arbeitet still im Hintergrund. Er gibt keine Nachricht aus, er erledigt einfach seinen Job. Jede Datei, die Claude erstellt oder bearbeitet, bekommt automatisch die richtigen Berechtigungen.

Praxisbeispiel 3: Änderungen protokollieren

Für größere Projekte ist es Gold wert, wenn ich nachvollziehen kann, was wann geändert wurde. PostToolUse ist der perfekte Ort dafür:

Python: Änderungen in Datenbank protokollieren

def protokolliere_aenderung(hook_daten):
    """Schreibt jede Änderung in die Audit-Tabelle"""

    werkzeug = hook_daten.get('tool_name')
    eingabe = hook_daten.get('tool_input', {})
    ergebnis = hook_daten.get('tool_result', {})
    sitzung = hook_daten.get('session_id')
    zeitstempel = hook_daten.get('timestamp')

    # In Datenbank schreiben
    cursor.execute("""
        INSERT INTO audit_log
        (sitzung_id, werkzeug, eingabe, ergebnis, zeitstempel)
        VALUES (%s, %s, %s, %s, %s)
    """, (
        sitzung,
        werkzeug,
        json.dumps(eingabe),
        json.dumps(ergebnis),
        zeitstempel
    ))
    connection.commit()

Mit diesem Protokoll kann ich später nachvollziehen:

Der Feedback-Mechanismus

Das Mächtigste an PostToolUse ist der Feedback-Mechanismus. Wenn ich eine Nachricht zurückgebe, sieht Claude diese Nachricht und kann darauf reagieren.

So funktioniert der Kreislauf

Schritt Was passiert Beispiel
1 Claude führt Aktion aus Bearbeitet eine PHP-Datei
2 PostToolUse feuert Hook prüft Syntax
3 Hook findet Problem Fehlende Klammer entdeckt
4 Hook gibt Feedback „Syntaxfehler in Zeile 42"
5 Claude sieht Feedback Erkennt das Problem
6 Claude korrigiert Fügt fehlende Klammer ein

Dieser Kreislauf entsteht ganz natürlich. Ich muss Claude nicht explizit sagen: „Wenn Du diesen Fehler siehst, dann tu das." Claude versteht den Kontext und reagiert entsprechend.

Selbstkorrigierender Workflow

Durch den Feedback-Mechanismus entsteht ein selbstkorrigierender Workflow. Fehler werden nicht erst am Ende einer langen Arbeitssitzung entdeckt, sondern sofort nach jeder einzelnen Änderung. Das spart Zeit, Nerven und vor allem: Token.

Was ich damit alles machen kann

Die Möglichkeiten sind vielfältig. Hier eine Sammlung von Ideen, die ich selbst nutze oder die mir begegnet sind:

Ein wichtiger Unterschied zu PreToolUse

Lass mich das noch einmal deutlich machen, weil es für das Verständnis wichtig ist:

Aspekt PreToolUse PostToolUse
Zeitpunkt VOR der Aktion NACH der Aktion
Kann blockieren? Ja Nein
Hat Ergebnis? Nein (noch nicht ausgeführt) Ja (Erfolg/Misserfolg)
Typischer Einsatz Validierung, Prävention Prüfung, Protokollierung, Feedback
Analogie Türsteher Qualitätsprüfer

Beide Ereignisse ergänzen sich wunderbar. PreToolUse verhindert Fehler, PostToolUse entdeckt sie. Zusammen bilden sie ein robustes Sicherheitsnetz.

Der doppelte Boden

In meinem Setup nutze ich beide Ereignisse: PreToolUse prüft, ob die geplante Aktion sinnvoll ist. PostToolUse prüft, ob das Ergebnis korrekt ist. So entsteht ein doppelter Boden, der die meisten Probleme abfängt, bevor sie größeren Schaden anrichten können.

Zusammenfassung

PostToolUse ist der Moment der Reflexion. Die Aktion ist geschehen, jetzt ist Zeit für:

Anders als PreToolUse kann PostToolUse nicht blockieren. Aber das macht nichts. Denn die wahre Stärke liegt im Feedback-Mechanismus: Eine Warnung hier, ein Hinweis dort, und Claude korrigiert sich selbst. Das ist eleganter als jedes Blockieren.

Stop: Der Abbruch

Manchmal muss man die Notbremse ziehen. Das Stop-Ereignis feuert genau dann: Wenn Du Escape drückst, Ctrl+C tippst oder auf andere Weise sagst: „Halt, stopp, nicht weiter!" Es ist der Moment, in dem alles zum Stillstand kommt.

Das ist nicht unbedingt etwas Schlechtes. Ein Abbruch kann viele Gründe haben: Du hast gemerkt, dass Du den falschen Auftrag gegeben hast. Die Operation dauert zu lange. Du brauchst die Rechenleistung für etwas anderes. Oder Du hast einfach Deine Meinung geändert.

Wann wird Stop ausgelöst?

Das Stop-Ereignis feuert, wenn der Benutzer eine laufende Claude-Operation aktiv unterbricht. Das kann zu verschiedenen Zeitpunkten passieren:

Der Zeitpunkt ist unvorhersehbar. Genau das macht den Stop-Hook so wichtig: Ich muss darauf vorbereitet sein, dass jederzeit der Stecker gezogen werden kann.

Die Notbremse im Zug

Ein Stop-Ereignis ist wie die Notbremse im Zug. Sie kann jederzeit gezogen werden, und dann muss alles geordnet zum Stillstand kommen. Keine Panik, kein Chaos, sondern ein kontrolliertes Anhalten. Der Zug bleibt nicht einfach stehen, er bremst sicher ab.

Was bekomme ich zu sehen?

Das Stop-Ereignis liefert mir wichtige Informationen darüber, was gerade passiert ist:

JSON: Stop Datenstruktur

{
  "event": "Stop",
  "session_id": "abc123...",
  "reason": "user_interrupt",
  "timestamp": "2026-01-13T12:15:00Z",
  "last_tool": "Edit",
  "last_tool_status": "in_progress"
}

Besonders interessant sind hier:

Mit diesen Informationen kann ich entscheiden, was aufgeräumt werden muss.

Warum ist das wichtig?

Ein Abbruch kann zu einem ungültigen Zustand führen. Beispiel: Claude war gerade dabei, eine Funktion umzubenennen. Die alte Referenz wurde gelöscht, aber die neue noch nicht erstellt. Jetzt ist der Code kaputt.

Oder: Ein langer Batch-Prozess wurde unterbrochen. 47 von 100 Dateien wurden verarbeitet. Welche 47? Wo wurde aufgehört?

Der Stop-Hook gibt mir die Chance, solche Situationen zu behandeln.

Praxisbeispiel 1: Offene Tasks markieren

Wenn ich ein Task-System verwende, möchte ich wissen, welche Aufgaben unterbrochen wurden:

Python: Tasks als unterbrochen markieren

#!/usr/bin/env python3
"""Stop Hook: Markiert unterbrochene Tasks"""

import json
import sys
from datetime import datetime

def markiere_unterbrochene_tasks(sitzung_id):
    """Findet und markiert alle laufenden Tasks dieser Sitzung"""
    cursor.execute("""
        UPDATE tasks
        SET status = 'unterbrochen',
            unterbrochen_am = %s,
            notiz = 'Abbruch durch Benutzer'
        WHERE sitzung_id = %s
          AND status = 'in_bearbeitung'
    """, (datetime.now(), sitzung_id))

    return cursor.rowcount

def main():
    hook_daten = json.load(sys.stdin)
    sitzung_id = hook_daten.get('session_id')

    try:
        anzahl = markiere_unterbrochene_tasks(sitzung_id)

        if anzahl > 0:
            print(json.dumps({
                "continue": True,
                "message": f"⏹️ {anzahl} Task(s) als unterbrochen markiert"
            }))
        else:
            print(json.dumps({"continue": True}))

    except Exception:
        # Bei Stop-Hooks: Niemals fehlschlagen!
        print(json.dumps({"continue": True}))

if __name__ == "__main__":
    main()

Der wichtige Teil hier: Der try/except Block um alles herum. Ein Stop-Hook darf niemals fehlschlagen. Der Benutzer hat „Stopp!" gesagt und erwartet, dass das System anhält. Nicht, dass es mit einer Fehlermeldung crasht.

Praxisbeispiel 2: Arbeitsstand sichern

Bei längeren Operationen kann es sinnvoll sein, den Fortschritt zu sichern:

Python: Fortschritt speichern

def sichere_fortschritt(sitzung_id):
    """Speichert den aktuellen Arbeitsstand"""

    fortschritt = {
        'sitzung_id': sitzung_id,
        'zeitpunkt': datetime.now().isoformat(),
        'bearbeitete_dateien': hole_bearbeitete_dateien(sitzung_id),
        'offene_aufgaben': hole_offene_aufgaben(sitzung_id),
        'letzter_schritt': hole_letzten_schritt(sitzung_id)
    }

    # In Datei speichern für späteren Neustart
    dateiname = f"/tmp/claude_fortschritt_{sitzung_id}.json"
    with open(dateiname, 'w') as f:
        json.dump(fortschritt, f, indent=2)

    return dateiname

So kann ich beim nächsten Start nachschauen: „Ah, hier wurde unterbrochen. Diese Dateien wurden bereits bearbeitet, diese noch nicht."

Praxisbeispiel 3: Temporäre Dateien aufräumen

Manche Operationen erstellen Zwischendateien. Die sollten nicht herumliegen bleiben:

Bash: Temporäre Dateien löschen

#!/bin/bash
# Stop Hook: Räumt temporäre Dateien auf

HOOK_DATEN=$(cat)
SITZUNG_ID=$(echo "$HOOK_DATEN" | jq -r '.session_id')

# Temporäre Dateien dieser Sitzung finden und löschen
TEMP_PATTERN="/tmp/claude_*_${SITZUNG_ID}*"

if ls $TEMP_PATTERN &>/dev/null; then
    ANZAHL=$(ls $TEMP_PATTERN 2>/dev/null | wc -l)
    rm -f $TEMP_PATTERN 2>/dev/null
    echo "{\"continue\": true, \"message\": \"🧹 $ANZAHL temporäre Datei(en) aufgeräumt\"}"
else
    echo '{"continue": true}'
fi

Die goldene Regel für Stop-Hooks

Es gibt eine Regel, die wichtiger ist als alle anderen:

Schnell und fehlerfrei

Ein Stop-Hook muss schnell sein und darf niemals fehlschlagen. Der Benutzer hat abgebrochen. Er will nicht warten. Er will schon gar nicht eine Fehlermeldung sehen. Fange alle Exceptions ab. Setze Timeouts. Beende immer sauber mit {"continue": true}.

Warum ist das so wichtig? Weil der Benutzer bereits ungeduldig ist. Er hat „Stopp" gesagt, weil etwas nicht stimmt oder weil er keine Zeit mehr hat. Das Letzte, was er jetzt braucht, ist ein Hook, der ewig läuft oder mit einem Fehler abbricht.

Was kann ich damit alles machen?

Hier eine Sammlung von Ideen:

Der Unterschied zu SessionEnd

Du fragst Dich vielleicht: „Was ist der Unterschied zwischen Stop und SessionEnd?"

Aspekt Stop SessionEnd
Auslöser Benutzer unterbricht aktiv Sitzung endet normal
Zeitpunkt Mitten in einer Operation Nach Abschluss aller Operationen
Erwartung Unvollständiger Zustand möglich Alles sollte abgeschlossen sein
Dringlichkeit Sehr hoch (schnell sein!) Normal

Stop ist der Notfall. SessionEnd ist der planmäßige Abschluss. Beide sind wichtig, aber sie erfordern unterschiedliche Reaktionen.

Zusammenfassung

Das Stop-Ereignis ist Deine Chance, bei einem Abbruch aufzuräumen. Markiere unterbrochene Tasks, sichere den Fortschritt, räume temporäre Dateien auf. Aber denk dran: Schnell sein und niemals fehlschlagen. Der Benutzer wartet.

Du kannst Dir die Aufzeichnung hier ansehen:

Als Mitglied der KI-Gemeinschaft kannst Du Dir die vollständige Aufzeichnung ansehen und auf alle weiteren Hook-Events zugreifen: SubagentStop, SessionEnd, Notification, PermissionRequest und PreCompact.

php if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true): ? oder [Anmelden](<?= AuthHelper::LOGIN_URL ?>?return=<?= urlencode($_SERVER['REQUEST_URI'] ?? '/claude-code-hooks-events') ?>) php endif; ?

SubagentStop: Der Sub-Task ist fertig

Kennst Du das Gefühl, wenn Du mehrere Leute losgeschickt hast, um verschiedene Dinge zu erledigen, und nach und nach kommen sie zurück mit ihren Ergebnissen? Genau das ist SubagentStop: Der Moment, in dem ein Helfer meldet: „Chef, ich bin fertig!"

Claude kann nämlich nicht nur selbst arbeiten, sondern auch kleine Helfer losschicken. Diese Helfer heißen „Subagents" und sind spezialisierte Mini-Claudes, die bestimmte Aufgaben besonders gut können.

Was sind Subagents überhaupt?

Ein Subagent ist wie ein Spezialist, den Du für eine bestimmte Aufgabe engagierst. Claude kann über das Task-Werkzeug verschiedene Typen starten:

Die Spezialisten im Team

Subagent-Typ Spezialgebiet Typischer Einsatz
Explore Codebase durchsuchen „Finde alle Dateien die X enthalten"
Plan Implementierung planen „Erstelle einen Plan für Feature Y"
Bash Terminal-Operationen „Führe diese Befehle aus"
General-Purpose Komplexe Aufgaben „Recherchiere und analysiere Z"

Das Schöne daran: Diese Subagents können parallel arbeiten. Während der eine die Codebase durchsucht, plant der andere schon die Implementierung. Das spart Zeit.

Wann wird SubagentStop ausgelöst?

SubagentStop feuert, sobald ein Subagent seine Arbeit beendet. Das kann verschiedene Gründe haben:

In jedem Fall bekomme ich eine Benachrichtigung: „Der Helfer ist zurück."

Der Bote kehrt zurück

Ein Subagent ist wie ein Bote, den Du in eine andere Stadt schickst. Du weißt nicht genau, wann er zurückkommt, aber Du weißt, dass er sich meldet, sobald er da ist. SubagentStop ist diese Meldung: „Ich bin zurück, hier ist, was ich gefunden habe."

Was bekomme ich zu sehen?

SubagentStop liefert mir alle wichtigen Informationen über den abgeschlossenen Helfer:

JSON: SubagentStop Datenstruktur

{
  "event": "SubagentStop",
  "session_id": "abc123...",
  "subagent_id": "sub_xyz789",
  "subagent_type": "Explore",
  "status": "completed",
  "result_summary": "Found 15 matching files",
  "duration_ms": 4500,
  "timestamp": "2026-01-13T12:20:00Z"
}

Besonders interessant:

Praxisbeispiel 1: Performance-Monitoring

Wenn ich mehrere Subagents einsetze, will ich wissen, wie schnell sie sind. Vielleicht ist ein bestimmter Typ immer langsam? Das kann ich tracken:

Python: Subagent-Laufzeiten erfassen

#!/usr/bin/env python3
"""SubagentStop Hook: Erfasst Performance-Metriken"""

import json
import sys

def erfasse_metrik(hook_daten):
    """Speichert Laufzeit und Status in Metriken-Tabelle"""

    cursor.execute("""
        INSERT INTO subagent_metriken
        (sitzung_id, subagent_id, typ, status, dauer_ms, zeitpunkt)
        VALUES (%s, %s, %s, %s, %s, %s)
    """, (
        hook_daten.get('session_id'),
        hook_daten.get('subagent_id'),
        hook_daten.get('subagent_type'),
        hook_daten.get('status'),
        hook_daten.get('duration_ms'),
        hook_daten.get('timestamp')
    ))
    connection.commit()

def main():
    hook_daten = json.load(sys.stdin)

    erfasse_metrik(hook_daten)

    # Warnung bei langsamen Subagents
    dauer = hook_daten.get('duration_ms', 0)
    typ = hook_daten.get('subagent_type', 'Unknown')

    if dauer > 30000:  # Mehr als 30 Sekunden
        print(json.dumps({
            "continue": True,
            "message": f"⏱️ {typ}-Agent brauchte {dauer/1000:.1f}s"
        }))
    else:
        print(json.dumps({"continue": True}))

if __name__ == "__main__":
    main()

Mit der Zeit sehe ich dann Muster: „Aha, der Explore-Agent ist immer schnell, aber der Plan-Agent braucht oft länger." Das hilft mir, die Arbeit besser zu planen.

Praxisbeispiel 2: Ergebnisse zusammenführen

Wenn mehrere Subagents parallel arbeiten, möchte ich ihre Ergebnisse am Ende zusammenführen:

Python: Ergebnisse aggregieren

def aggregiere_ergebnisse(sitzung_id, subagent_id, ergebnis):
    """Sammelt Ergebnisse aller Subagents einer Sitzung"""

    # Ergebnis speichern
    ergebnisse = lade_bisherige_ergebnisse(sitzung_id)
    ergebnisse[subagent_id] = ergebnis
    speichere_ergebnisse(sitzung_id, ergebnisse)

    # Prüfen ob alle Subagents fertig sind
    erwartete = hole_erwartete_subagents(sitzung_id)
    fertige = set(ergebnisse.keys())

    if erwartete == fertige:
        # Alle fertig! Zusammenfassung erstellen
        zusammenfassung = erstelle_zusammenfassung(ergebnisse)
        return f"✅ Alle {len(fertige)} Subagents fertig:\n{zusammenfassung}"

    return None

So weiß ich genau, wann alle Helfer zurück sind und kann dann den nächsten Schritt einleiten.

Praxisbeispiel 3: Fehlerbehandlung

Nicht jeder Subagent ist erfolgreich. Manchmal gibt es Fehler. Die will ich mitbekommen:

Python: Fehlgeschlagene Subagents behandeln

def main():
    hook_daten = json.load(sys.stdin)

    status = hook_daten.get('status')
    typ = hook_daten.get('subagent_type')
    subagent_id = hook_daten.get('subagent_id')

    if status != 'completed':
        # Fehler protokollieren
        protokolliere_fehler(hook_daten)

        # Entscheiden: Retry oder aufgeben?
        versuche = hole_bisherige_versuche(subagent_id)

        if versuche < 3:
            nachricht = f"⚠️ {typ}-Agent fehlgeschlagen (Versuch {versuche}/3)"
        else:
            nachricht = f"❌ {typ}-Agent endgültig fehlgeschlagen"

        print(json.dumps({
            "continue": True,
            "message": nachricht
        }))
    else:
        print(json.dumps({"continue": True}))

Warum ist das wichtig?

Subagents sind mächtig, aber sie sind auch Blackboxen. Ich schicke sie los und hoffe, dass sie zurückkommen. SubagentStop gibt mir die Kontrolle zurück:

Orchestrierung

Mit SubagentStop kann ich komplexe Workflows orchestrieren: Mehrere Subagents parallel starten, auf ihre Ergebnisse warten, diese zusammenführen und dann den nächsten Schritt einleiten. Das ist wie ein Dirigent, der sein Orchester koordiniert.

Was kann ich damit alles machen?

Zusammenfassung

SubagentStop ist der Moment, in dem ein Helfer zurückkommt. Nutze ihn, um Ergebnisse zu sammeln, Performance zu messen und Fehler zu behandeln. Je mehr Subagents Du einsetzt, desto wertvoller wird dieser Hook.

SessionEnd: Der Abschluss

Jeder Tag hat ein Ende, jede Arbeitssitzung ebenfalls. SessionEnd ist der Moment, in dem die Lichter ausgehen und ich aufräumen kann, bevor ich gehe. Es ist der planmäßige, geordnete Abschluss einer Claude-Code-Sitzung.

Anders als beim Stop-Ereignis, das plötzlich und unerwartet kommt, ist SessionEnd vorhersehbar. Die Arbeit ist getan, der Benutzer hat „Tschüss" gesagt, und jetzt ist Zeit für die letzten Handgriffe.

Wann wird SessionEnd ausgelöst?

SessionEnd feuert, wenn eine Claude-Code-Sitzung regulär beendet wird. Das kann verschiedene Ursachen haben:

In all diesen Fällen habe ich einen Moment Zeit, um sauber abzuschließen.

Feierabend

SessionEnd ist wie der Feierabend nach einem Arbeitstag. Du räumst Deinen Schreibtisch auf, speicherst Deine Dateien, fährst den Computer herunter und schaltest das Licht aus. Alles in Ruhe, ohne Hektik.

Was bekomme ich zu sehen?

SessionEnd liefert mir eine Zusammenfassung der gesamten Sitzung:

JSON: SessionEnd Datenstruktur

{
  "event": "SessionEnd",
  "session_id": "abc123...",
  "duration_seconds": 1847,
  "tool_calls": 42,
  "prompts": 15,
  "timestamp": "2026-01-13T12:30:00Z"
}

Das sind wertvolle Informationen:

Mit diesen Zahlen kann ich interessante Statistiken erstellen.

Praxisbeispiel 1: Sitzungsstatistiken speichern

Ich sammle gerne Statistiken über meine Arbeitssitzungen. Das hilft mir zu verstehen, wie ich Claude nutze und wo ich effizienter werden kann:

Python: Sitzungsstatistiken erfassen

#!/usr/bin/env python3
"""SessionEnd Hook: Speichert Sitzungsstatistiken"""

import json
import sys
from datetime import datetime

def speichere_statistiken(hook_daten):
    """Schreibt Sitzungsstatistiken in die Datenbank"""

    cursor.execute("""
        INSERT INTO sitzungs_statistiken
        (sitzung_id, dauer_sekunden, werkzeug_aufrufe, eingaben, beendet_am)
        VALUES (%s, %s, %s, %s, %s)
    """, (
        hook_daten.get('session_id'),
        hook_daten.get('duration_seconds'),
        hook_daten.get('tool_calls'),
        hook_daten.get('prompts'),
        datetime.now()
    ))
    connection.commit()

def main():
    hook_daten = json.load(sys.stdin)

    speichere_statistiken(hook_daten)

    # Kurze Zusammenfassung ausgeben
    dauer = hook_daten.get('duration_seconds', 0)
    minuten = dauer // 60
    werkzeuge = hook_daten.get('tool_calls', 0)

    print(json.dumps({
        "continue": True,
        "message": f"📊 Sitzung: {minuten} Min, {werkzeuge} Werkzeuge"
    }))

if __name__ == "__main__":
    main()

Nach einigen Wochen kann ich dann auswerten: Wie lange sind meine typischen Sitzungen? Welche Tage sind besonders produktiv? Wie viele Werkzeuge nutze ich durchschnittlich?

Praxisbeispiel 2: Aufräumen

Während einer Sitzung entstehen oft temporäre Dateien, Cache-Einträge und andere Überbleibsel. SessionEnd ist der perfekte Moment zum Aufräumen:

Bash: Temporäre Dateien aufräumen

#!/bin/bash
# SessionEnd Hook: Räumt auf

HOOK_DATEN=$(cat)
SITZUNG_ID=$(echo "$HOOK_DATEN" | jq -r '.session_id')

# Temporäre Dateien dieser Sitzung
rm -f /tmp/claude_*_${SITZUNG_ID}* 2>/dev/null

# Alte Cache-Dateien (älter als 7 Tage)
find /tmp -name "claude_cache_*" -mtime +7 -delete 2>/dev/null

# Leere Verzeichnisse aufräumen
find /tmp -type d -empty -name "claude_*" -delete 2>/dev/null

echo '{"continue": true}'

Praxisbeispiel 3: Abschluss-Report

Manchmal möchte ich am Ende einer Sitzung wissen, was eigentlich alles passiert ist. Ein automatischer Report kann da helfen:

Python: Sitzungs-Report erstellen

def erstelle_report(sitzung_id):
    """Erstellt eine Zusammenfassung der Sitzung"""

    # Bearbeitete Dateien abrufen
    dateien = hole_bearbeitete_dateien(sitzung_id)

    # Tasks abrufen
    tasks = hole_abgeschlossene_tasks(sitzung_id)

    # Fehler abrufen
    fehler = hole_aufgetretene_fehler(sitzung_id)

    report = {
        'sitzung_id': sitzung_id,
        'bearbeitete_dateien': len(dateien),
        'dateien_liste': dateien[:10],  # Erste 10
        'abgeschlossene_tasks': len(tasks),
        'fehler': len(fehler)
    }

    # Report speichern
    speichere_report(report)

    return report

Ein wichtiger Hinweis

So schön SessionEnd auch ist, es gibt eine Einschränkung:

Keine Garantie

SessionEnd wird nicht garantiert ausgelöst. Bei einem Absturz, einem Kill-Signal oder einem Stromausfall gibt es kein SessionEnd. Kritische Daten sollten deshalb kontinuierlich gespeichert werden, nicht erst am Ende. SessionEnd ist für „Nice-to-have"-Aufräumarbeiten, nicht für lebensnotwendige Operationen.

Das bedeutet: Wichtige Daten speichere ich laufend (zum Beispiel in PostToolUse). SessionEnd nutze ich für Statistiken, Reports und Aufräumarbeiten, die nicht kritisch sind.

Der Unterschied zu Stop

Zur Erinnerung, weil es wichtig ist:

Aspekt Stop SessionEnd
Auslöser Benutzer unterbricht aktiv Sitzung endet normal
Zeitpunkt Mitten in einer Operation Nach Abschluss aller Operationen
Dringlichkeit Hoch (schnell sein!) Normal (Zeit für Statistiken)
Analogie Notbremse Feierabend

Was kann ich damit alles machen?

Zusammenfassung

SessionEnd ist der geordnete Abschluss, der Feierabend nach getaner Arbeit. Nutze ihn für Statistiken, Reports und Aufräumarbeiten. Aber vergiss nicht: Er ist nicht garantiert. Kritische Daten gehören in andere Hooks.

Notification: Die Glocke läutet

In jedem System gibt es Momente, in denen etwas Wichtiges passiert und jemand davon erfahren sollte. Notification ist die Glocke, die läutet, wenn es Neuigkeiten gibt. Nicht laut und aufdringlich, aber hörbar genug, um Aufmerksamkeit zu erregen.

In einem mittelalterlichen Dorf hängt auf dem Kirchturm eine Glocke. Sie läutet zur vollen Stunde, bei Gefahr, bei freudigen Ereignissen. Die Dorfbewohner wissen: Wenn die Glocke klingt, ist etwas passiert. Notification ist diese Glocke im Claude-Code-System.

Wann läutet die Glocke?

Das Notification-Ereignis feuert bei verschiedenen System-Ereignissen:

All diese Ereignisse haben etwas gemeinsam: Sie passieren „nebenbei", während ich mit etwas anderem beschäftigt bin. Die Glocke informiert mich, ohne meine aktuelle Arbeit zu unterbrechen.

Der Unterschied zum Postboten

Anders als ein Postbote, der an der Tür klingelt und wartet, bis jemand aufmacht, ist Notification eine Kirchenglocke: Sie läutet, und wer sie hört, kann reagieren. Wer sie nicht hört oder ignoriert, dem wird nichts aufgezwungen. Notification-Hooks können den Ablauf nicht blockieren. Sie sind reine Beobachter.

Was bekomme ich zu hören?

Wenn die Glocke läutet, erfahre ich, was passiert ist:

JSON: Notification Datenstruktur

{
  "event": "Notification",
  "session_id": "abc123...",
  "notification_type": "background_task_complete",
  "title": "Task abgeschlossen",
  "message": "Hintergrund-Befehl beendet (Exit-Code 0)",
  "severity": "info",
  "timestamp": "2026-01-13T12:35:00Z"
}

Die wichtigsten Felder im Überblick:

Die verschiedenen Glockentöne

Nicht jede Glocke klingt gleich. Je nach Anlass gibt es verschiedene Töne:

Typ Bedeutung Analogie
background_task_complete Hintergrund-Aufgabe fertig Feierabendglocke
context_warning Kontext-Limit nähert sich Warnglocke
system_warning System-Warnung Alarmglocke
update_available Update verfügbar Nachrichtenglocke

Praxisbeispiel 1: Benachrichtigungen weiterleiten

Ich möchte wissen, was in meinen Claude-Code-Sitzungen passiert, auch wenn ich nicht direkt davor sitze. Also leite ich wichtige Benachrichtigungen an meinen Messenger weiter:

Python: Benachrichtigungen an Slack senden

#!/usr/bin/env python3
"""Notification Hook: Leitet wichtige Meldungen an Slack weiter"""

import json
import sys
import requests

# Nur diese Typen weiterleiten
WICHTIGE_TYPEN = ['context_warning', 'system_warning']

def sende_an_slack(titel, nachricht, schwere):
    """Sendet eine Nachricht an Slack"""

    # Emoji je nach Schweregrad
    emoji = {
        'info': ':information_source:',
        'warning': ':warning:',
        'error': ':rotating_light:'
    }.get(schwere, ':bell:')

    requests.post(SLACK_WEBHOOK_URL, json={
        'text': f"{emoji} *{titel}*\n{nachricht}"
    })

def main():
    hook_daten = json.load(sys.stdin)

    typ = hook_daten.get('notification_type')

    if typ in WICHTIGE_TYPEN:
        sende_an_slack(
            hook_daten.get('title'),
            hook_daten.get('message'),
            hook_daten.get('severity')
        )

    print(json.dumps({"continue": True}))

if __name__ == "__main__":
    main()

So erfahre ich von Warnungen, egal wo ich gerade bin. Die Glocke läutet, und mein Telefon vibriert.

Praxisbeispiel 2: Kontext-Warnung intelligent behandeln

Wenn das Kontext-Limit näher rückt, möchte ich vorbereitet sein. Ich speichere den aktuellen Stand automatisch:

Python: Bei Kontext-Warnung sichern

def behandle_kontext_warnung(hook_daten):
    """Reagiert auf Kontext-Limit-Warnungen"""

    sitzung_id = hook_daten.get('session_id')

    # Aktuellen Fortschritt sichern
    speichere_sitzungszustand(sitzung_id)

    # Offene Tasks exportieren
    exportiere_offene_tasks(sitzung_id)

    # Arbeitsverzeichnis protokollieren
    protokolliere_arbeitsverzeichnis(sitzung_id)

    return {
        'gesichert': True,
        'sitzung': sitzung_id,
        'zeitstempel': datetime.now().isoformat()
    }

Wenn dann tatsächlich der Kontext voll ist und die Sitzung kompaktiert wird, habe ich bereits alles Wichtige gesichert.

Praxisbeispiel 3: Benachrichtigungs-Statistiken

Über die Zeit sammeln sich viele Benachrichtigungen an. Ein Hook kann sie aggregieren und mir später einen Überblick geben:

Python: Benachrichtigungen zählen

def zaehle_benachrichtigung(hook_daten):
    """Zählt Benachrichtigungen nach Typ und Schweregrad"""

    typ = hook_daten.get('notification_type')
    schwere = hook_daten.get('severity')
    datum = datetime.now().strftime('%Y-%m-%d')

    cursor.execute("""
        INSERT INTO benachrichtigungs_statistik
        (datum, typ, schweregrad, anzahl)
        VALUES (%s, %s, %s, 1)
        ON DUPLICATE KEY UPDATE anzahl = anzahl + 1
    """, (datum, typ, schwere))

    connection.commit()

Nach einigen Wochen kann ich auswerten: Wie viele Kontext-Warnungen bekomme ich? Gibt es Muster? Welche Tage sind besonders „laut"?

Die Weisheit der Glocke

Die Glocke lehrt uns etwas Wichtiges: Nicht jede Information muss sofort verarbeitet werden. Manche Dinge sind gut zu wissen, aber nicht dringend. Notification gibt mir die Möglichkeit zu entscheiden:

Reine Beobachtung

Anders als andere Hooks kann Notification den Ablauf nicht beeinflussen. Du kannst nicht „blockieren" oder „modifizieren". Du kannst nur beobachten, protokollieren und weiterleiten. Das ist gewollt: Benachrichtigungen sollen informieren, nicht kontrollieren.

Was kann ich damit alles machen?

Zusammenfassung

Notification ist die Glocke des Systems. Sie läutet, wenn etwas passiert ist. Du entscheidest, ob Du hinschaust, ob Du es weitersagst, oder ob Du es ignorierst. Die Glocke zwingt nichts auf, sie informiert nur. Nutze sie weise.

PermissionRequest: Der Schlüsselmeister

In jedem System gibt es Bereiche, die nicht für jedermann zugänglich sind. Manche Türen sind verschlossen, und nur wer den richtigen Schlüssel hat, darf eintreten. PermissionRequest ist der Schlüsselmeister, der entscheidet, wer Zugang bekommt.

Denk an ein großes Gebäude: Im Erdgeschoss kann jeder frei herumlaufen. Aber manche Etagen, manche Räume, sind nur mit Schlüsselkarte zugänglich. Der Schlüsselmeister sitzt am Empfang und prüft: „Haben Sie die Berechtigung für diesen Bereich?"

Wann wird der Schlüsselmeister gerufen?

PermissionRequest feuert, wenn Claude eine Aktion ausführen möchte, die besondere Berechtigungen erfordert:

Anders als andere Hooks ist PermissionRequest nicht nur ein Beobachter. Er hat echte Macht: Er kann Anfragen genehmigen oder ablehnen.

Macht und Verantwortung

Der Schlüsselmeister trägt eine große Verantwortung. Wenn er zu streng ist, wird die Arbeit behindert. Wenn er zu locker ist, können Schäden entstehen. Die Kunst liegt in der Balance.

Was bekommt der Schlüsselmeister zu sehen?

Bei jeder Anfrage erhält der Hook detaillierte Informationen:

JSON: PermissionRequest Datenstruktur

{
  "event": "PermissionRequest",
  "session_id": "abc123...",
  "permission_type": "bash_execution",
  "requested_action": {
    "tool": "Bash",
    "command": "rm -rf /tmp/build/*"
  },
  "risk_level": "medium",
  "timestamp": "2026-01-13T12:40:00Z"
}

Die wichtigsten Informationen:

Die Entscheidungsmöglichkeiten

Der Schlüsselmeister hat drei Optionen:

Entscheidung Bedeutung Wann sinnvoll?
allow Genehmigung erteilen Bekannte, sichere Operationen
deny Ablehnung Gefährliche oder verbotene Aktionen
(keine Antwort) Normale Abfrage Bei Unsicherheit, manuelle Prüfung

Praxisbeispiel 1: Automatische Genehmigung für sichere Befehle

Manche Befehle sind so harmlos, dass ich sie automatisch genehmigen kann. Das spart Zeit und unterbricht den Arbeitsfluss nicht:

Python: Sichere Befehle automatisch genehmigen

#!/usr/bin/env python3
"""PermissionRequest Hook: Auto-Genehmigung für sichere Befehle"""

import json
import sys
import re

# Diese Muster sind sicher und werden automatisch genehmigt
SICHERE_MUSTER = [
    r'^git\s+status',              # Git Status
    r'^git\s+log',                 # Git Log
    r'^git\s+diff',                # Git Diff
    r'^ls\s+',                     # Verzeichnisinhalt
    r'^php\s+-l\s+',               # PHP Syntax-Check
    r'^cat\s+/var/log/',           # Log-Dateien lesen
    r'^npm\s+test',                # Tests ausführen
]

def ist_sicher(befehl):
    """Prüft, ob ein Befehl als sicher gilt"""
    for muster in SICHERE_MUSTER:
        if re.match(muster, befehl):
            return True
    return False

def main():
    hook_daten = json.load(sys.stdin)

    aktion = hook_daten.get('requested_action', {})
    befehl = aktion.get('command', '')

    if ist_sicher(befehl):
        print(json.dumps({
            "hookSpecificOutput": {
                "permissionDecision": "allow",
                "permissionDecisionReason": "Auto: Sicherer Befehl"
            }
        }))
    else:
        # Keine automatische Entscheidung, normale Abfrage
        print(json.dumps({"continue": True}))

if __name__ == "__main__":
    main()

Mit dieser Konfiguration werden harmlose Lese-Operationen sofort durchgewunken, ohne dass ich jedes Mal bestätigen muss.

Praxisbeispiel 2: Gefährliche Befehle blockieren

Es gibt Befehle, die ich unter keinen Umständen automatisch ausführen lassen möchte. Der Schlüsselmeister kann sie kategorisch ablehnen:

Python: Gefährliche Befehle blockieren

# Diese Muster werden IMMER abgelehnt
VERBOTENE_MUSTER = [
    r'^rm\s+-rf\s+/',                # Rekursives Löschen ab Root
    r'^chmod\s+777',                 # Unsichere Berechtigungen
    r'^curl.*\|\s*bash',             # Piped Execution
    r'^wget.*\|\s*sh',               # Piped Execution
    r'>\s*/etc/',                    # Schreiben in /etc
    r'^sudo\s+',                     # Sudo-Befehle
]

def ist_verboten(befehl):
    """Prüft, ob ein Befehl verboten ist"""
    for muster in VERBOTENE_MUSTER:
        if re.search(muster, befehl):
            return True
    return False

def main():
    hook_daten = json.load(sys.stdin)

    aktion = hook_daten.get('requested_action', {})
    befehl = aktion.get('command', '')

    if ist_verboten(befehl):
        print(json.dumps({
            "hookSpecificOutput": {
                "permissionDecision": "deny",
                "permissionDecisionReason": "Blockiert: Gefährlicher Befehl"
            }
        }))
    elif ist_sicher(befehl):
        # ... (wie oben)
    else:
        print(json.dumps({"continue": True}))

Diese Befehle werden niemals automatisch genehmigt. Selbst wenn ich sie manuell bestätigen wollte, würde der Hook sie abfangen.

Praxisbeispiel 3: Kontextabhängige Entscheidungen

Manchmal hängt die Entscheidung vom Kontext ab. Ein Befehl, der in einem Projekt sicher ist, kann in einem anderen gefährlich sein:

Python: Projektspezifische Regeln

import os

# Projektspezifische Regeln
PROJEKT_REGELN = {
    '/var/www/dev.karlkratz.de': {
        'erlaubt': [r'^npm\s+', r'^php\s+', r'^git\s+'],
        'verboten': [r'^rm\s+-rf']
    },
    '/var/www/produktion': {
        'erlaubt': [r'^git\s+status'],  # Nur lesende Git-Befehle
        'verboten': [r'^git\s+push', r'^rm\s+']  # Kein Push, kein Löschen
    }
}

def pruefe_mit_kontext(befehl, arbeitsverzeichnis):
    """Prüft Berechtigung basierend auf Projektkontext"""

    regeln = PROJEKT_REGELN.get(arbeitsverzeichnis, {})

    # Erst verbotene Muster prüfen
    for muster in regeln.get('verboten', []):
        if re.match(muster, befehl):
            return 'deny'

    # Dann erlaubte Muster
    for muster in regeln.get('erlaubt', []):
        if re.match(muster, befehl):
            return 'allow'

    return None  # Keine automatische Entscheidung

So kann ich in der Entwicklungsumgebung großzügiger sein als in der Produktionsumgebung.

Die goldene Regel des Schlüsselmeisters

Der weise Schlüsselmeister folgt einem Prinzip:

Im Zweifel: Fragen

Automatische Genehmigungen sind bequem, aber gefährlich. Beschränke sie auf wirklich unbedenkliche Operationen. Bei allem anderen: Lass die normale Berechtigungsabfrage zu. Es ist besser, einmal zu viel zu fragen als einmal zu wenig.

Was kann ich damit alles machen?

Ein Wort zur Vorsicht

Der Schlüsselmeister hat echte Macht. Mit großer Macht kommt große Verantwortung:

Zusammenfassung

PermissionRequest ist der Schlüsselmeister des Systems. Er entscheidet, welche Aktionen genehmigt werden und welche nicht. Nutze diese Macht weise: Automatisiere das Sichere, blockiere das Gefährliche, und frage im Zweifel lieber einmal mehr.

PreCompact: Der letzte Koffer

Eine lange Reise steht an, und Du darfst nur einen Koffer mitnehmen. Der Raum ist begrenzt, Du musst entscheiden: Was ist wirklich wichtig? Was muss unbedingt mit? PreCompact ist der Moment, in dem Du Deinen Koffer packst, bevor die Reise losgeht.

Claude hat ein begrenztes Gedächtnis, das sogenannte Kontextfenster. Wenn dieses voll wird, muss Platz geschaffen werden. Ältere Nachrichten werden zusammengefasst oder vergessen. Dabei können wichtige Details verloren gehen. PreCompact ist Deine letzte Chance, die wichtigen Dinge zu retten.

Wann wird PreCompact ausgelöst?

PreCompact feuert kurz bevor die Komprimierung beginnt. Das passiert, wenn:

In diesem Moment habe ich noch Zugriff auf den vollen Kontext, aber gleich nicht mehr.

Der Koffer-Moment

Es ist wie beim Packen vor einer Reise: Du siehst all Deine Sachen, aber Du weißt, dass nicht alles in den Koffer passt. Jetzt musst Du entscheiden, was wirklich wichtig ist. Was Du nicht einpackst, wirst Du auf der Reise nicht dabei haben.

Was bekomme ich zu sehen?

Der Hook liefert wichtige Informationen über den bevorstehenden Verlust:

JSON: PreCompact Datenstruktur

{
  "event": "PreCompact",
  "session_id": "abc123...",
  "current_context_tokens": 180000,
  "max_context_tokens": 200000,
  "messages_to_compact": 45,
  "timestamp": "2026-01-13T12:45:00Z"
}

Die wichtigsten Informationen:

Im Beispiel sind 180.000 von 200.000 Tokens belegt, und 45 Nachrichten werden gleich zusammengefasst.

Praxisbeispiel 1: Wichtige Entscheidungen sichern

Während einer langen Arbeitssitzung treffe ich viele Entscheidungen. Manche davon sind wichtig für den weiteren Verlauf. Bevor sie vergessen werden, sichere ich sie:

Python: Kritische Informationen sichern

#!/usr/bin/env python3
"""PreCompact Hook: Sichert wichtige Kontext-Informationen"""

import json
import sys
from datetime import datetime

def sichere_kontext(hook_daten):
    """Speichert kritische Informationen vor der Komprimierung"""

    sitzung_id = hook_daten.get('session_id')

    kontext_sicherung = {
        'sitzung_id': sitzung_id,
        'zeitstempel': datetime.now().isoformat(),
        'tokens_vor_komprimierung': hook_daten.get('current_context_tokens'),
        'komprimierte_nachrichten': hook_daten.get('messages_to_compact')
    }

    # In Datei speichern
    dateiname = f"/tmp/kontext_sicherung_{sitzung_id}.json"
    with open(dateiname, 'w') as f:
        json.dump(kontext_sicherung, f, indent=2)

    return dateiname

def main():
    hook_daten = json.load(sys.stdin)

    dateiname = sichere_kontext(hook_daten)

    print(json.dumps({
        "continue": True,
        "message": f"Kontext gesichert: {dateiname}"
    }))

if __name__ == "__main__":
    main()

Diese Sicherung kann ich später nutzen, um den Kontext wiederherzustellen oder nachzuvollziehen, was vor der Komprimierung passiert ist.

Praxisbeispiel 2: Offene Tasks exportieren

Wenn der Kontext komprimiert wird, könnten offene Aufgaben vergessen werden. Um das zu verhindern, exportiere ich sie in eine externe Datei:

Python: Offene Tasks sichern

def exportiere_offene_tasks(sitzung_id):
    """Exportiert alle offenen Tasks vor der Komprimierung"""

    # Offene Tasks aus der Datenbank holen
    cursor.execute("""
        SELECT inhalt, status, erstellt_am
        FROM sitzungs_tasks
        WHERE sitzung_id = %s AND status != 'completed'
        ORDER BY erstellt_am
    """, (sitzung_id,))

    offene_tasks = cursor.fetchall()

    if offene_tasks:
        export = {
            'sitzung_id': sitzung_id,
            'exportiert_am': datetime.now().isoformat(),
            'grund': 'PreCompact - Kontext-Komprimierung',
            'offene_tasks': [
                {'inhalt': t[0], 'status': t[1], 'erstellt': t[2]}
                for t in offene_tasks
            ]
        }

        speichere_task_export(export)

        return len(offene_tasks)

    return 0

Nach der Komprimierung weiß ich dann immer noch, was zu tun ist.

Praxisbeispiel 3: Eigene Zusammenfassung erstellen

Claude erstellt bei der Komprimierung eine automatische Zusammenfassung. Aber manchmal möchte ich eine eigene, angepasste Version:

Python: Eigene Zusammenfassung

def erstelle_zusammenfassung(hook_daten):
    """Erstellt eine strukturierte Zusammenfassung"""

    sitzung_id = hook_daten.get('session_id')

    zusammenfassung = {
        'meta': {
            'sitzung': sitzung_id,
            'komprimiert_am': datetime.now().isoformat(),
            'tokens_vor': hook_daten.get('current_context_tokens')
        },
        'bearbeitete_dateien': hole_bearbeitete_dateien(sitzung_id),
        'getroffene_entscheidungen': hole_entscheidungen(sitzung_id),
        'offene_fragen': hole_offene_fragen(sitzung_id),
        'naechste_schritte': hole_geplante_schritte(sitzung_id)
    }

    # In menschenlesbarem Format speichern
    dateiname = f"/tmp/zusammenfassung_{sitzung_id}.md"
    with open(dateiname, 'w') as f:
        f.write(formatiere_als_markdown(zusammenfassung))

    return dateiname

Diese Zusammenfassung kann ich später wieder einlesen, um den Faden aufzunehmen.

Die Weisheit des Packers

Der weise Packer weiß: Man kann nicht alles mitnehmen. Aber man kann sicherstellen, dass das Wichtigste dabei ist. PreCompact gibt Dir diese Möglichkeit.

Letzte Chance

PreCompact ist wirklich die letzte Chance. Nach der Komprimierung sind die alten Nachrichten nur noch als Zusammenfassung vorhanden. Was Du jetzt nicht sicherst, ist möglicherweise für immer verloren. Nutze diesen Moment.

Was kann ich damit alles machen?

Zusammenfassung

PreCompact ist der Koffer-Moment: Du packst das Wichtigste ein, bevor die Reise weitergeht. Nutze diesen Hook, um kritische Informationen zu retten. Was Du jetzt sicherst, steht Dir auch nach der Komprimierung zur Verfügung. Was Du nicht sicherst, ist möglicherweise für immer weg.

Zusammenfassung: Alle Events im Lebenszyklus

Hier noch einmal der vollständige Überblick über alle Hook-Events und wann sie feuern:

Der Lebenszyklus einer Claude-Code-Sitzung

╔═══════════════════════════════════════════════════════════════╗
║                    SESSION LIFECYCLE                           ║
╠═══════════════════════════════════════════════════════════════╣
║  SessionStart ─────────────────────────────────────────────── ║
║       │                                                        ║
║       ▼                                                        ║
║  ┌─────────────────────────────────────────────────────────┐  ║
║  │  UserPromptSubmit                                        │  ║
║  │       │                                                  │  ║
║  │       ▼                                                  │  ║
║  │  ┌─────────────────────────────────────────────────┐    │  ║
║  │  │  PreToolUse ──▶ Tool ──▶ PostToolUse             │    │  ║
║  │  │       ▲                    │                     │    │  ║
║  │  │       └────────────────────┘ (wiederholen)       │    │  ║
║  │  └─────────────────────────────────────────────────┘    │  ║
║  │                                                          │  ║
║  │  Optionale Events:                                       │  ║
║  │  ├─ Stop (bei Abbruch)                                  │  ║
║  │  ├─ SubagentStop (bei Sub-Task-Ende)                    │  ║
║  │  ├─ Notification (bei System-Events)                    │  ║
║  │  ├─ PermissionRequest (bei Berechtigungsanfrage)        │  ║
║  │  └─ PreCompact (vor Kontext-Komprimierung)               │  ║
║  └─────────────────────────────────────────────────────────┘  ║
║       │                                                        ║
║       ▼                                                        ║
║  SessionEnd ───────────────────────────────────────────────── ║
╚═══════════════════════════════════════════════════════════════╝

Jedes Event hat seinen Platz und seine Aufgabe. Zusammen bilden sie ein mächtiges System, mit dem Du Claude Code an Deine Bedürfnisse anpassen kannst.

Praxisbeispiel: Wie ich mit dem Regelwerk zuerst immens viele Token verbraucht und dann sehr viel gespart habe

Ich habe einen sehr gewissenhaften Assistenten gebaut. Dieser Assistent prüft bei jeder Handlung, ob bestimmte Regeln eingehalten werden. Das ist grundsätzlich eine gute Sache, schließlich möchte ich keine Fehler machen.

Nun ist es aber so: Dieser Assistent hat ein sehr dickes Regelbuch mit über 200 Regeln. Und jedes Mal, wenn auch nur ein Buchstabe geändert wird, holt er das komplette Buch hervor, blättert durch alle 200 Seiten und prüft jede einzelne Regel.

Das Problem dabei? Die meisten Regeln sind in dem Moment völlig irrelevant. Wenn gerade ein deutscher Text geschrieben wird, braucht es keine Regeln für französische Grammatik. Wenn an einer Tabellenkalkulation gearbeitet wird, sind die Regeln für Bildbearbeitung unwichtig.

Die Ausgangssituation

Mein Prüfsystem lädt bei jeder Änderung alle 200+ Regeln aus der Datenbank. Erst danach wird im Programm gefiltert, welche Regeln tatsächlich relevant sind. Das ist etwa so, als würde ein Bibliothekar bei jeder Frage erst einmal alle Bücher aus dem Regal holen und dann die meisten ungelesen zurückstellen.

Was genau passiert da eigentlich?

Wenn Claude Code eine Datei bearbeitet, passiert Folgendes:

  1. Du gibst einen Auftrag: „Ändere bitte diese PHP-Datei."
  2. Claude bereitet die Änderung vor: Das Werkzeug „Bearbeiten" wird aktiviert.
  3. Der Ereignis-Auslöser feuert: „PreToolUse", also „Vor der Werkzeug-Nutzung".
  4. Das Prüfsystem springt an: Es soll sicherstellen, dass die Änderung keine Regeln verletzt.
  5. Hier wird es ineffizient: Das System lädt ALLE Regeln, obwohl nur ein Bruchteil relevant ist.

In Zahlen ausgedrückt: Bei einer typischen Arbeitssitzung mit 50 Dateiänderungen werden über 10.000 Regeln geladen, von denen mehr als 9.000 sofort wieder verworfen werden. Das ist nicht nur langsam, sondern auch verschwenderisch.

Ein Blick unter die Motorhaube

Für die technisch Interessierten zeige ich hier, wie die ursprüngliche Datenbankabfrage aussah. Aber auch ohne Programmierkenntnisse ist die Struktur leicht zu verstehen:

SQL: Die alte Abfrage

-- Die alte Abfrage: Hole ALLES
SELECT regeln.*, vertraege.schluessel
FROM regeln
JOIN vertraege ON regeln.vertrag_id = vertraege.id
WHERE vertraege.status = 'aktiv'
  AND regeln.ist_aktiv = 1

-- Ergebnis: ~200 Datensätze
-- Davon relevant: ~10-20
-- Verschwendung: ~90%

Die Abfrage sagt im Grunde: „Gib mir alle aktiven Regeln aus allen aktiven Verträgen." Das ist etwa so, als würdest Du in einer Bibliothek sagen: „Gib mir alle Bücher, die nicht aussortiert wurden." Nicht besonders zielführend, wenn Du eigentlich nur ein Kochbuch suchst.

Die Filterung kam viel zu spät

Was passierte nach dem Laden aller Regeln? Das Programm musste diese Arbeit erledigen:

All diese Prüfungen hätten bereits in der Datenbank stattfinden können. Stattdessen wurden die Daten erst übertragen, dann im Programm gefiltert. Das ist, als würdest Du im Restaurant erst alle Gerichte der Speisekarte bestellen und dann bei der Lieferung sagen: „Ach, ich bin eigentlich Vegetarier."

Das eigentliche Problem

Je später im Ablauf gefiltert wird, desto mehr Ressourcen werden verschwendet. Daten müssen übertragen, zwischengespeichert und verarbeitet werden, nur um dann festzustellen, dass sie nicht gebraucht werden.

Die Idee: Früher und präziser filtern

Die Lösung liegt auf der Hand: Ich muss der Datenbank beibringen, von Anfang an nur die relevanten Regeln herauszugeben. Dafür brauche ich zusätzliche Informationen in der Datenbank, quasi Etiketten, die jeder Regel sagen, wann und wo sie gebraucht wird.

Das ist wie ein gut organisiertes Gewürzregal. Statt alle Gewürze auf einen Haufen zu werfen, sortierst Du sie:

Wenn Du dann einen milden italienischen Salat zubereitest, greifst Du gezielt in die richtige Ecke und nicht erst durch alle Gewürze.

Die sechs Filterebenen

Ich habe das Regelwerk in sechs Ebenen organisiert. Jede Ebene filtert präziser:

Von grob zu fein

Ebene Fragestellung Beispielwerte
1. Ereignis Bei welchem Ereignis prüfen? Sitzungsstart, Vor Werkzeug, Nach Werkzeug
2. Wann Wann genau wird geprüft? vor_werkzeug, nach_werkzeug, sitzungsstart
3. Wo Wo gilt die Regel? global, projektspezifisch
4. Werkzeug Bei welchem Werkzeug? Bearbeiten, Schreiben, Lesen, Konsole
5. Dateitypen Für welche Dateitypen? .php, .py, .js, .css
6. Verzeichnisse In welchen Verzeichnissen? src/, tests/, templates/

Diese sechs Ebenen arbeiten wie ein Trichter: Jede Ebene reduziert die Anzahl der übrig bleibenden Regeln weiter, bis am Ende nur noch die wirklich relevanten übrig sind.

Was musste ich dafür ändern?

Die Datenbank brauchte zwei neue Informationsfelder. In der Fachsprache nennt man das „Spalten hinzufügen". Hier ist, was ich ergänzt habe:

SQL: Schema-Erweiterung

-- Neue Information 1: Bei welchem Ereignis wird diese Regel geprüft?
ALTER TABLE regeln
ADD COLUMN ausloeser_typ ENUM('vor_werkzeug','nach_werkzeug','sitzungsstart')
DEFAULT 'vor_werkzeug';

-- Neue Information 2: Welche Werkzeuge lösen diese Regel aus?
ALTER TABLE regeln
ADD COLUMN ausloeser_werkzeuge JSON DEFAULT NULL;

-- Verzeichnis für schnelles Nachschlagen
CREATE INDEX idx_ausloeser
ON regeln(ausloeser_typ, ist_aktiv);

Was bedeutet das in einfachen Worten?

Warum diese Unterscheidung wichtig ist

Du fragst Dich vielleicht: „Warum brauchen wir sowohl einen Geltungsbereich als auch einen Auslöser-Typ? Ist das nicht das Gleiche?"

Gute Frage! Diese beiden Dimensionen sind tatsächlich unabhängig voneinander, sie beantworten unterschiedliche Fragen:

Dimension Beantwortet die Frage Beispiel
Geltungsbereich WO gilt die Regel? „Diese Regel gilt überall" vs. „Diese Regel gilt nur im Projekt XY"
Auslöser-Typ WANN wird geprüft? „Prüfe VOR der Änderung" vs. „Prüfe NACH der Änderung"
Werkzeuge Bei WELCHER Aktion? „Nur beim Bearbeiten" vs. „Nur bei Konsolenbefehlen"

Ein konkretes Beispiel: Eine Regel könnte so definiert sein:

Diese Regel würde also überall gelten, aber nur nach der Ausführung von Konsolenbefehlen geprüft werden. Die drei Dimensionen ergänzen sich, ohne sich zu überschneiden.

Die neue, optimierte Abfrage

Mit den zusätzlichen Informationen kann die Datenbank jetzt viel gezielter arbeiten. Hier ist die neue Abfrage, wieder mit Erklärungen:

Python: Optimierte Regelabfrage

def hole_relevante_regeln(projekt, dateiendung, dateipfad, werkzeug, ausloeser_typ='vor_werkzeug'):
    """
    Lädt nur die Regeln, die wirklich gebraucht werden.

    Parameter:
    - projekt: In welchem Projekt arbeiten wir?
    - dateiendung: Welche Dateiendung hat die Datei? (.php, .py, ...)
    - dateipfad: Wo liegt die Datei? (src/..., tests/..., ...)
    - werkzeug: Welches Werkzeug wird verwendet? (Bearbeiten, Schreiben, ...)
    - ausloeser_typ: Wann wird geprüft? (vor_werkzeug, nach_werkzeug, ...)
    """

    abfrage = """
        SELECT r.*, v.schluessel, v.name AS vertragsname
        FROM regeln r
        JOIN vertraege v ON r.vertrag_id = v.id
        WHERE v.status = 'aktiv'
          AND r.ist_aktiv = 1
          -- Filter 1: Nur Regeln für diesen Auslöser-Typ
          AND r.ausloeser_typ = %s
          -- Filter 2: Nur Regeln für dieses Werkzeug (oder alle, wenn nicht eingeschränkt)
          AND (r.ausloeser_werkzeuge IS NULL
               OR JSON_CONTAINS(r.ausloeser_werkzeuge, %s))
          -- Filter 3: Nur Regeln für diese Dateiendung (oder alle, wenn nicht eingeschränkt)
          AND (r.gilt_fuer_endungen IS NULL
               OR JSON_CONTAINS(r.gilt_fuer_endungen, %s))
          -- Filter 4: Nur Regeln für dieses Projekt (oder globale Regeln)
          AND (v.geltungsbereich = 'global'
               OR JSON_CONTAINS(v.projekt_schluessel, %s))
        ORDER BY r.schweregrad DESC
    """

    cursor.execute(abfrage, (
        ausloeser_typ,
        json.dumps(werkzeug),
        json.dumps(dateiendung),
        json.dumps(projekt)
    ))
    return cursor.fetchall()

Was passiert hier? Die Datenbank filtert jetzt selbst:

  1. Nur Regeln, die zum aktuellen Zeitpunkt geprüft werden sollen
  2. Nur Regeln, die für das verwendete Werkzeug relevant sind
  3. Nur Regeln, die für den Dateityp gelten
  4. Nur Regeln, die im aktuellen Projekt oder überall gelten

Das Ergebnis: Statt 200 Regeln kommen nur noch 10 bis 20 zurück. Die restlichen 180+ werden gar nicht erst übertragen.

Wie habe ich das umgestellt?

Eine wichtige Frage bei solchen Änderungen ist: Wie stellt man ein laufendes System um, ohne dass etwas kaputtgeht? Ich habe das in drei Phasen gemacht:

Phase 1: Struktur erweitern

Zuerst habe ich die neuen Informationsfelder zur Datenbank hinzugefügt, aber mit Standardwerten. Das bedeutet: Alle bestehenden Regeln funktionieren weiter wie bisher. Ein leeres Feld wird als „gilt für alles" interpretiert.

Analogie: Ich baue einen neuen Schrank neben den alten, ohne den alten abzureißen.

Phase 2: Regeln klassifizieren

Dann habe ich jede bestehende Regel mit den neuen Informationen versehen. Bei welchem Ereignis soll sie geprüft werden? Für welche Werkzeuge gilt sie? Vieles davon konnte ich automatisch aus den bestehenden Daten ableiten.

Analogie: Ich beschrifte alle Gewürze im alten Regal, bevor ich sie in das neue, sortierte Regal umräume.

Phase 3: Abfragen umstellen

Erst ganz zum Schluss habe ich das Programm auf die neue, optimierte Abfrage umgestellt. Zu diesem Zeitpunkt waren alle Daten bereits korrekt klassifiziert.

Analogie: Erst wenn alle Gewürze im neuen Regal stehen, ändere ich meine Gewohnheit und greife dorthin.

Diese schrittweise Vorgehensweise nennt man „sanfte Migration". Zu keinem Zeitpunkt war das System in einem kaputten Zustand. Hätte etwas nicht funktioniert, hätte ich jederzeit zur alten Version zurückkehren können.

Was habe ich damit erreicht?

Hier sind die messbaren Verbesserungen:

Was ich messe Vorher Nachher Verbesserung
Regeln pro PHP-Bearbeitung ~200 ~15 92% weniger
Regeln pro Python-Bearbeitung ~200 ~11 95% weniger
Datenbankabfrage ~45 ms ~8 ms 82% schneller
Filterung im Programm ~120 ms ~5 ms 96% schneller
Gesamtzeit pro Prüfung ~165 ms ~13 ms 92% schneller

Bei 50 Änderungen pro Arbeitssitzung spart das etwa 7,5 Sekunden reine Wartezeit. Das klingt vielleicht nicht nach viel, aber mir geht es um etwas anderes: Es werden immens viele Token eingespart. Bei jeder Prüfung fallen 90% weniger Daten an, die verarbeitet werden müssen. Du bleibst mehr im Flow, weil das System schneller reagiert. Und im Schnitt bist Du einfach 90% effizienter unterwegs. Den kleinen Schwaben in mir freut so etwas ungemein.

Der Hebel-Effekt

Eine kleine Optimierung an der richtigen Stelle, nämlich dort, wo die Daten gefiltert werden, hat einen überproportional großen Effekt. Das liegt daran, dass diese Stelle bei jeder einzelnen Operation durchlaufen wird. Je früher im Ablauf Du optimierst, desto größer der Hebel.

Was ich daraus gelernt habe

Diese Optimierung hat mir einige wertvolle Erkenntnisse gebracht, die auch für andere Situationen gelten:

Zusammenfassung: Alle Ereignisse im Überblick

Zum Abschluss dieses Kapitels und dieses gesamten Artikels über Ereignis-Auslöser hier noch einmal der vollständige Lebenszyklus einer Claude-Code-Sitzung:

Sitzungs-Lebenszyklus

┌─────────────────────────────────────────────────────────────┐
│                   SITZUNGS-LEBENSZYKLUS                     │
├─────────────────────────────────────────────────────────────┤
│  Sitzungsstart (SessionStart)                               │
│       ↓                                                     │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  Eingabe-Übermittlung (UserPromptSubmit)            │   │
│  │       ↓                                             │   │
│  │  ┌─────────────────────────────────────────────┐   │   │
│  │  │  Vor-Werkzeug → Werkzeug → Nach-Werkzeug   │   │   │
│  │  │       ↑              ↓                      │   │   │
│  │  │       └──────────────┘ (wiederholen)       │   │   │
│  │  └─────────────────────────────────────────────┘   │   │
│  │       ↓                                             │   │
│  │  [Stopp] (bei Abbruch durch Benutzer)              │   │
│  │  [Unteragent-Stopp] (bei Ende eines Unteragenten)  │   │
│  │  [Benachrichtigung] (bei Systemmeldungen)          │   │
│  │  [Berechtigungsanfrage] (bei sensiblen Aktionen)   │   │
│  │  [Vor-Komprimierung] (bei vollem Kontextspeicher)  │   │
│  └─────────────────────────────────────────────────────┘   │
│       ↓                                                     │
│  Sitzungsende (SessionEnd)                                  │
└─────────────────────────────────────────────────────────────┘

Jedes dieser Ereignisse bietet Dir die Möglichkeit, in den Ablauf einzugreifen, sei es zur Validierung, zur Protokollierung, zur Optimierung oder zur Automatisierung. Die Kunst liegt darin, das richtige Ereignis für den richtigen Zweck zu wählen.

Der Schlüssel zum Verständnis

Ereignis-Auslöser sind wie Türklingeln an verschiedenen Stellen eines Gebäudes. Wenn Du weißt, welche Klingel wann ertönt, kannst Du zur richtigen Zeit am richtigen Ort sein und genau das tun, was in diesem Moment gebraucht wird.

Ich freue mich, dass Du Dir die Zeit genommen hast, bis hierher zu lesen (das ist mittlerweile etwas Besonderes)!

Liebe Grüße, Dein Karl

P.S.: Wenn Du noch mehr über solche Themen erfahren möchtest, dann komm doch in die KI-Gemeinschaft!