Aufbau und Voraussetzungen
Hooks reagieren auf CLI-Ereignisse. Sie sind reale Prozess- oder Netzwerkaktionen, keine Promptvorlagen. Projektquellen benötigen Zustimmung; globale Hooks gelten als vertrauenswürdige Benutzerquelle. Prüfen Sie ausführbaren Inhalt, Ziel und Datenfluss vor einer Freigabe.
Die Datei .lurus/hooks.json enthält direkt eine Eventmap, keine zusätzliche äußere hooks-Ebene. Für globalen Einsatz verwenden Sie ~/.lurus/hooks.json. Alternativ können Sie in den Einstellungen hooks sowie – abhängig vom Geltungsbereich – globalHooks oder projectHooks verwenden. Eventarrays aus diesen Quellen werden aneinander angehängt; verschachtelte Einstellungen werden nicht allgemein zusammengeführt. Gehen Sie nicht davon aus, dass Änderungen automatisch neu geladen werden. Starten Sie danach eine neue CLI-Sitzung.
Command-Hooks erhalten auf stdin ein JSON-Objekt:
{
"event": "PreToolUse",
"context": { "toolName": "Write", "toolInput": { "path": "example.txt" } },
"cwd": "/path/to/project"
}
toolInput ist der tatsächlich vom Tool verwendete Input, keine für alle Werkzeuge einheitliche Form. Ausführung im richtigen Projekt-CWD und vorhandenes node sind Voraussetzungen der folgenden Beispiele. Nicht in echten Nutzerglobals experimentieren.
Write gezielt blockieren
Legen Sie .lurus/hooks/block-write.cjs an:
process.stdin.resume();
process.stdin.on('end', () => {
console.log(
JSON.stringify({
decision: 'deny',
reason: 'Write is blocked by this project hook.',
}),
);
});
Und .lurus/hooks.json:
{
"PreToolUse": [
{
"matcher": "^Write$",
"hooks": [
{
"type": "command",
"command": "node .lurus/hooks/block-write.cjs",
"timeout": 30000
}
]
}
]
}
Nach Neustart und bewusster Projektfreigabe wird ein Write-Aufruf durch deny verhindert. Edit, MultiEdit, Shellprozesse und andere Mutationen sind damit nicht gesperrt. Dieses Beispiel ist kein universelles Schreib- oder Securitygate. Global ist dieselbe Eventstruktur möglich; verwenden Sie im globalen Befehl einen tatsächlich auflösbaren geprüften Scriptpfad, nicht einen Projektpfad, der in anderen Projekten fehlt.
Toolereignisse wirklich protokollieren
Nur stderr auszugeben erzeugt nicht automatisch eine nutzerseitig sichtbare Logdatei. Ein vollständiges Beispiel .lurus/hooks/log-tool.cjs schreibt bewusst ausschließlich Metadaten:
const { appendFileSync } = require('node:fs');
const { join } = require('node:path');
let input = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
input += chunk;
});
process.stdin.on('end', () => {
const payload = JSON.parse(input);
const record = {
event: payload.event,
toolName: payload.context.toolName,
isError: payload.context.isError,
};
appendFileSync(
join(payload.cwd, '.lurus/hooks/tool-events.log'),
JSON.stringify(record) + '\n',
);
console.log(JSON.stringify({ decision: 'allow' }));
});
Ergänzen Sie im selben .lurus/hooks.json das Event:
{
"PostToolUse": [
{
"matcher": "^(Write|Edit)$",
"hooks": [
{
"type": "command",
"command": "node .lurus/hooks/log-tool.cjs",
"timeout": 30000
}
]
}
]
}
Die beiden Beispiele sind getrennte vollständige Eventzuordnungen. Für gemeinsame Verwendung übernehmen Sie beide Eventschlüssel in ein JSON-Objekt. Nach passenden erfolgreichen Toolaufrufen werden Zeilen in .lurus/hooks/tool-events.log angehängt. Verwenden Sie einen vorhandenen Hooksordner und protokollieren Sie keine Tooleingaben, Ergebnisse, Tokens oder Secrets. Ein Logfehler stoppt die Ausführung nicht zuverlässig; siehe Fail-open.
Alle Events und tatsächliche Wirkung
Jeder Hook erhält {event, context, cwd}. Definitionen und Aktionen laufen nacheinander; deny stoppt die restlichen Hooks. Nur dafür vorgesehene Events blockieren auch die CLI-Aktion, wie in der Tabelle beschrieben.
| Event | Contextfelder | Tatsächliche Wirkung |
|---|---|---|
SessionStart | sessionId, trigger: startup | Begleitet den Start, blockiert ihn aber nicht; Zusatzkontext wird dabei nicht übernommen. |
SessionEnd | sessionId, trigger: exit | Begleitet das Beenden, ohne es zu blockieren. |
PreToolUse | toolName, toolInput | deny verhindert den Toolaufruf; Zusatzkontext wird verarbeitet. |
PostToolUse | toolName, toolResult, isError: false | Zusatzkontext; keine Rücknahme einer bereits geschehenen Aktion. |
PostToolUseFailure | toolName, toolResult, isError: true | Zusatzkontext nach Fehler; nicht jeder frühe Abbruch feuert dieses Event. |
UserPromptSubmit | prompt | deny stoppt den Prompt; Zusatzkontext wird verarbeitet. |
SubagentStart | subagentId, subagentType | Begleitet den Start eines Unteragenten, ohne ihn zu blockieren. |
SubagentStop | subagentId, subagentType | Begleitet den Abschluss, ohne Ergebnisse zurückzunehmen. |
Stop | trigger: stop | Übernimmt Zusatzkontext einmalig; ein weiterer Agentlauf ist nicht garantiert. |
PreCompact | trigger: compact, messageCount, estimatedTokens, filesRead | Begleitet die Komprimierung, ohne sie zu blockieren. |
Matcher und Felder
Der Matcher wird gegen context.toolName, sonst context.trigger geprüft. Fehlen beide, passt jeder Matcher. Leer und * passen immer. Ohne Stern wird erst exakt, dann als Regex verglichen; mit Stern entsteht eine verankerte Regexumsetzung, andere Sonderzeichen regulärer Ausdrücke werden nicht automatisch maskiert. Dies ist kein Standard-Globmatcher. Verwenden Sie beispielsweise ^(Write|Edit)$, um eine konkrete Toolgruppe auszuwählen.
| Feld | Typ / Default |
|---|---|
<Event> | Array von Definitionen, Eventname in JSON exakt schreiben. |
matcher | Optionaler string wie ^Write$. |
hooks | Array von Actions pro Definition. |
type: command | command: string, optional timeout: number in ms. |
type: http | url: string, optional method: POST|PUT|PATCH, headers: Record<string,string>, timeout: number. |
timeout | Standardmäßig 30.000 ms pro Aktion, kein Gesamtlaufbudget. |
method | Standardmäßig POST. |
Antwortvertrag und Fail-open
stdout gehört dem Antwortvertrag, Diagnose auf stderr:
{
"decision": "deny",
"reason": "Explain why the action is blocked",
"additionalContext": "Optional guidance for the agent"
}
- Exit 0: JSON mit
decision: allow|deny, optionalreason/additionalContext. - Exit 2: deny; Grund aus stderr/stdout.
askist kein geparster Entscheidungswert.- Ungültiges JSON, andere Exitcodes, Ausführungsfehler und HTTP-Statuscodes außerhalb von 2xx führen zu Fail-open: Die Aktion wird nicht automatisch blockiert.
- HTTP-Antworten mit 2xx-Status verwenden denselben JSON-Antwortvertrag.
- Shelltimeout sendet SIGTERM; kein garantiertes hartes Beenden aller Kindprozesse.
Ein fehlender Hook, Scriptfehler oder Timeout ist deshalb kein verlässlicher Schutzwall. Secrets, Pfade und Prozessrechte müssen zusätzlich durch die eigentlichen Permissions geschützt sein.
HTTP-Hook
Für einen bereits vorhandenen bewusst lokalen Entwicklerendpoint, der JSON antwortet:
{
"UserPromptSubmit": [
{
"hooks": [
{
"type": "http",
"url": "http://127.0.0.1:8765/hooks/prompt",
"method": "POST",
"headers": { "Content-Type": "application/json" },
"timeout": 30000
}
]
}
]
}
Der Requestbody enthält die Eventdaten, hier also auch den Prompt. Der vorhandene Endpunkt muss zum Beispiel {"decision":"allow"} mit einem HTTP-Status im 2xx-Bereich zurückgeben. Stellen Sie nicht auf einen fremden öffentlichen Endpunkt um, ohne die Datenfreigabe zu prüfen. Authorization, Cookie, Proxy-Authorization, X-API-Key und CRLF in Headerwerten sind gesperrt; keine Authsecrets als Hookheader einchecken.
Generator, Validierung und Umgebung
Achtung: Der Generator schreibt Projektdateien und kann gleichnamige Skripte überschreiben. Prüfen Sie Namen und vorhandene Dateien vor der Ausführung.
/create-hook PreToolUse block-write
/create-hook PostToolUse log-tool --ts
/hooks validate
Der Generator erstellt Projektdateien und ergänzt hooks.json, wenn für das Event eine Vorlage vorhanden ist. Bei der Eventeingabe wird Großschreibung ignoriert; in JSON müssen die Namen exakt stimmen. Shellvorlagen werden ausführbar gemacht; TypeScriptvorlagen benötigen npx tsx. Generierte Befehle können absolute Pfade verwenden.
/hooks validate prüft nur einen Teil der Projektkonfiguration, nicht sämtliche Matcher, HTTP-Einstellungen, Timeouts oder Sicherheitswirkungen. Es gibt keine allgemeinen Create- oder Reloadoptionen. Hookprozesse erhalten nur zugelassene Umgebungsvariablen; LURUS_HOOK_PASS_ENV ergänzt kommaseparierte Namen. Jeder weitergereichte Wert ist für den Hookprozess vollständig sichtbar und erfordert bewusste Freigabe.
Fehlersuche
Ein falsches Arbeitsverzeichnis, fehlendes node oder ein fehlender Skriptpfad führt zu einem Ausführungsfehler mit Fail-open. Wird kein Event verarbeitet, prüfen Sie Schreibweise und Matcher. Blockiert deny nicht, prüfen Sie die Eventtabelle; nachträgliche Events können nichts zurücknehmen. Fehlt die Logdatei, prüfen Sie Zielordner und Schreibzugriff. Starten Sie nach Dateiänderungen eine neue Sitzung.