Agentverhalten mit Hooks abfangen und steuern
Fangen Sie Agentverhalten an wichtigen Ausführungspunkten mit Hooks ab und passen Sie es an
Hooks sind Callback-Funktionen, die Ihren Code als Reaktion auf Agent-Ereignisse ausführen, z. B. wenn ein Tool aufgerufen wird, eine Sitzung startet oder die Ausführung stoppt. Mit Hooks können Sie:
- Gefährliche Operationen blockieren, bevor sie ausgeführt werden, z. B. destruktive Shell-Befehle oder nicht autorisierter Dateizugriff
- Alle Tool-Aufrufe protokollieren und überprüfen für Compliance, Debugging oder Analytik
- Eingaben und Ausgaben transformieren, um Daten zu bereinigen, Anmeldedaten einzufügen oder Dateipfade umzuleiten
- Menschliche Genehmigung anfordern für sensible Aktionen wie Datenbankschreibvorgänge oder API-Aufrufe
- Sitzungslebenszyklus verfolgen, um den Status zu verwalten, Ressourcen freizugeben oder Benachrichtigungen zu senden
Dieser Leitfaden behandelt die Funktionsweise von Hooks, deren Konfiguration und bietet Beispiele für häufige Muster wie das Blockieren von Tools, das Ändern von Eingaben und das Weiterleiten von Benachrichtigungen.
Funktionsweise von Hooks
Ein Ereignis wird ausgelöst
Während der Agent-Ausführung passiert etwas und das SDK löst ein Ereignis aus: Ein Tool wird aufgerufen (PreToolUse), ein Tool gibt ein Ergebnis zurück (PostToolUse), ein Subagent startet oder stoppt, der Agent ist untätig oder die Ausführung ist beendet. Siehe die vollständige Liste der Ereignisse.
Das SDK sammelt registrierte Hooks
Das SDK prüft auf Hooks, die für diesen Ereignistyp registriert sind. Dies umfasst Callback-Hooks, die Sie in options.hooks übergeben, und Shell-Befehls-Hooks aus Einstellungsdateien, wenn der entsprechende settingSources oder setting_sources Eintrag aktiviert ist, was für Standard-query()-Optionen der Fall ist.
Matcher filtern, welche Hooks ausgeführt werden
Wenn ein Hook ein matcher Muster hat (z. B. "Write|Edit"), testet das SDK es gegen das Ziel des Ereignisses (z. B. den Tool-Namen). Hooks ohne Matcher werden für jedes Ereignis dieses Typs ausgeführt.
Callback-Funktionen werden ausgeführt
Jede übereinstimmende Hook-Callback-Funktion erhält Eingaben über das, was passiert: den Tool-Namen, seine Argumente, die Sitzungs-ID und andere ereignisspezifische Details.
Ihr Callback gibt eine Entscheidung zurück
Nach dem Ausführen von Operationen (Protokollierung, API-Aufrufe, Validierung) gibt Ihr Callback ein Ausgabeobjekt zurück, das dem Agent mitteilt, was zu tun ist: die Operation zulassen, blockieren, die Eingabe ändern oder Kontext in das Gespräch einfügen.
Das folgende Beispiel bringt diese Schritte zusammen. Es registriert einen PreToolUse Hook (Schritt 1) mit einem "Write|Edit" Matcher (Schritt 3), sodass der Callback nur für Datei-Schreib-Tools ausgelöst wird. Wenn ausgelöst, erhält der Callback die Eingabe des Tools (Schritt 4), prüft, ob der Dateipfad auf eine .env-Datei abzielt, und gibt permissionDecision: "deny" zurück, um die Operation zu blockieren (Schritt 5):
import asyncio
from claude_agent_sdk import (
AssistantMessage,
ClaudeSDKClient,
ClaudeAgentOptions,
HookMatcher,
ResultMessage,
)
# Define a hook callback that receives tool call details
async def protect_env_files(input_data, tool_use_id, context):
# Extract the file path from the tool's input arguments
file_path = input_data["tool_input"].get("file_path", "")
file_name = file_path.split("/")[-1]
# Block the operation if targeting a .env file
if file_name == ".env":
return {
"hookSpecificOutput": {
"hookEventName": input_data["hook_event_name"],
"permissionDecision": "deny",
"permissionDecisionReason": "Cannot modify .env files",
}
}
# Return empty object to allow the operation
return {}
async def main():
options = ClaudeAgentOptions(
hooks={
# Register the hook for PreToolUse events
# The matcher filters to only Write and Edit tool calls
"PreToolUse": [HookMatcher(matcher="Write|Edit", hooks=[protect_env_files])]
}
)
async with ClaudeSDKClient(options=options) as client:
await client.query("Update the database configuration")
async for message in client.receive_response():
# Filter for assistant and result messages
if isinstance(message, (AssistantMessage, ResultMessage)):
print(message)
asyncio.run(main())
import { query, HookCallback, PreToolUseHookInput } from "@anthropic-ai/claude-agent-sdk";
// Define a hook callback with the HookCallback type
const protectEnvFiles: HookCallback = async (input, toolUseID, { signal }) => {
// Cast input to the specific hook type for type safety
const preInput = input as PreToolUseHookInput;
// Cast tool_input to access its properties (typed as unknown in the SDK)
const toolInput = preInput.tool_input as Record<string, unknown>;
const filePath = toolInput?.file_path as string;
const fileName = filePath?.split("/").pop();
// Block the operation if targeting a .env file
if (fileName === ".env") {
return {
hookSpecificOutput: {
hookEventName: preInput.hook_event_name,
permissionDecision: "deny",
permissionDecisionReason: "Cannot modify .env files"
}
};
}
// Return empty object to allow the operation
return {};
};
for await (const message of query({
prompt: "Update the database configuration",
options: {
hooks: {
// Register the hook for PreToolUse events
// The matcher filters to only Write and Edit tool calls
PreToolUse: [{ matcher: "Write|Edit", hooks: [protectEnvFiles] }]
}
}
})) {
// Filter for assistant and result messages
if (message.type === "assistant" || message.type === "result") {
console.log(message);
}
}
Verfügbare Hooks
Das SDK bietet Hooks für verschiedene Phasen der Agent-Ausführung. Einige Hooks sind in beiden SDKs verfügbar, während andere nur für TypeScript verfügbar sind.
| Hook-Ereignis | Python SDK | TypeScript SDK | Was löst es aus | Beispiel-Anwendungsfall |
|---|---|---|---|---|
PreToolUse |
Ja | Ja | Tool-Aufrufanforderung (kann blockiert oder geändert werden) | Gefährliche Shell-Befehle blockieren |
PostToolUse |
Ja | Ja | Tool-Ausführungsergebnis | Alle Dateiänderungen im Audit-Trail protokollieren |
PostToolUseFailure |
Ja | Ja | Tool-Ausführungsfehler | Tool-Fehler behandeln oder protokollieren |
PostToolBatch |
Nein | Ja | Ein vollständiger Batch von Tool-Aufrufen wird aufgelöst, einmal pro Batch vor dem nächsten Modellaufruf | Konventionen einmal für den gesamten Batch einfügen |
UserPromptSubmit |
Ja | Ja | Benutzer-Prompt-Übermittlung | Zusätzlichen Kontext in Prompts einfügen |
Stop |
Ja | Ja | Agent-Ausführung stoppt | Sitzungsstatus vor dem Beenden speichern |
SubagentStart |
Ja | Ja | Subagent-Initialisierung | Parallele Task-Spawning verfolgen |
SubagentStop |
Ja | Ja | Subagent-Fertigstellung | Ergebnisse aus parallelen Tasks aggregieren |
PreCompact |
Ja | Ja | Anforderung zur Gesprächskomprimierung | Vollständiges Transkript vor der Zusammenfassung archivieren |
PermissionRequest |
Ja | Ja | Berechtigungsdialog würde angezeigt | Benutzerdefinierte Berechtigungsbehandlung |
SessionStart |
Nein | Ja | Sitzungsinitialisierung | Protokollierung und Telemetrie initialisieren |
SessionEnd |
Nein | Ja | Sitzungsbeendigung | Temporäre Ressourcen bereinigen |
Notification |
Ja | Ja | Agent-Statusmeldungen | Agent-Status-Updates an Slack oder PagerDuty senden |
Setup |
Nein | Ja | Sitzungssetup/Wartung | Initialisierungsaufgaben ausführen |
TeammateIdle |
Nein | Ja | Teammate wird untätig | Arbeit neu zuweisen oder benachrichtigen |
TaskCompleted |
Nein | Ja | Hintergrund-Task wird abgeschlossen | Ergebnisse aus parallelen Tasks aggregieren |
ConfigChange |
Nein | Ja | Konfigurationsdatei ändert sich | Einstellungen dynamisch neu laden |
WorktreeCreate |
Nein | Ja | Git Worktree erstellt | Isolierte Workspaces verfolgen |
WorktreeRemove |
Nein | Ja | Git Worktree entfernt | Workspace-Ressourcen bereinigen |
Hooks konfigurieren
Um einen Hook zu konfigurieren, übergeben Sie ihn im hooks Feld Ihrer Agent-Optionen (ClaudeAgentOptions in Python, das options Objekt in TypeScript):
options = ClaudeAgentOptions(
hooks={"PreToolUse": [HookMatcher(matcher="Bash", hooks=[my_callback])]}
)
async with ClaudeSDKClient(options=options) as client:
await client.query("Your prompt")
async for message in client.receive_response():
print(message)
for await (const message of query({
prompt: "Your prompt",
options: {
hooks: {
PreToolUse: [{ matcher: "Bash", hooks: [myCallback] }]
}
}
})) {
console.log(message);
}
Die hooks Option ist ein Wörterbuch (Python) oder Objekt (TypeScript), wobei:
- Schlüssel Hook-Ereignisnamen sind (z. B.
'PreToolUse','PostToolUse','Stop') - Werte Arrays von Matchern sind, die jeweils ein optionales Filtermuster und Ihre Callback-Funktionen enthalten
Matcher
Verwenden Sie Matcher, um zu filtern, wann Ihre Callbacks ausgelöst werden. Das matcher Feld ist eine Regex-Zeichenkette, die gegen einen anderen Wert abgeglichen wird, je nach Hook-Ereignistyp. Beispielsweise werden Tool-basierte Hooks gegen den Tool-Namen abgeglichen, während Notification Hooks gegen den Benachrichtigungstyp abgeglichen werden. Siehe die Claude Code Hooks-Referenz für die vollständige Liste der Matcher-Werte für jeden Ereignistyp.
| Option | Typ | Standard | Beschreibung |
|---|---|---|---|
matcher |
string |
undefined |
Regex-Muster, das gegen das Filterfeld des Ereignisses abgeglichen wird. Für Tool-Hooks ist dies der Tool-Name. Integrierte Tools umfassen Bash, Read, Write, Edit, Glob, Grep, WebFetch, Agent und andere (siehe Tool-Eingabetypen für die vollständige Liste). MCP-Tools verwenden das Muster mcp__<server>__<action>. |
hooks |
HookCallback[] |
- | Erforderlich. Array von Callback-Funktionen, die ausgeführt werden, wenn das Muster übereinstimmt |
timeout |
number |
60 |
Timeout in Sekunden |
Verwenden Sie das matcher Muster, um nach Möglichkeit spezifische Tools anzusteuern. Ein Matcher mit 'Bash' wird nur für Bash-Befehle ausgeführt, während das Weglassen des Musters Ihre Callbacks für jedes Vorkommen des Ereignisses ausführt. Beachten Sie, dass Matcher für Tool-basierte Hooks nur nach Tool-Namen filtern, nicht nach Dateipfaden oder anderen Argumenten. Um nach Dateipfad zu filtern, prüfen Sie tool_input.file_path in Ihrem Callback.
Tool-Namen entdecken: Siehe Tool-Eingabetypen für die vollständige Liste der integrierten Tool-Namen, oder fügen Sie einen Hook ohne Matcher hinzu, um alle Tool-Aufrufe Ihrer Sitzung zu protokollieren.
MCP-Tool-Benennung: MCP-Tools beginnen immer mit mcp__ gefolgt vom Servernamen und der Aktion: mcp__<server>__<action>. Wenn Sie beispielsweise einen Server namens playwright konfigurieren, werden seine Tools mcp__playwright__browser_screenshot, mcp__playwright__browser_click usw. benannt. Der Servername kommt aus dem Schlüssel, den Sie in der mcpServers Konfiguration verwenden.
Callback-Funktionen
Eingaben
Jeder Hook-Callback erhält drei Argumente:
- Eingabedaten: ein typisiertes Objekt mit Ereignisdetails. Jeder Hook-Typ hat seine eigene Eingabeform (z. B.
PreToolUseHookInputenthälttool_nameundtool_input, währendNotificationHookInputmessageenthält). Siehe die vollständigen Typdefinitionen in den TypeScript und Python SDK-Referenzen.- Alle Hook-Eingaben teilen
session_id,cwdundhook_event_name. agent_idundagent_typewerden ausgefüllt, wenn der Hook in einem Subagent ausgelöst wird. In TypeScript befinden sich diese in der Basis-Hook-Eingabe und sind für alle Hook-Typen verfügbar. In Python sind sie nur aufPreToolUse,PostToolUseundPostToolUseFailurevorhanden.
- Alle Hook-Eingaben teilen
- Tool-Verwendungs-ID (
str | None/string | undefined): korreliertPreToolUseundPostToolUseEreignisse für denselben Tool-Aufruf. - Kontext: In TypeScript enthält eine
signalEigenschaft (AbortSignal) für Abbruch. In Python ist dieses Argument für zukünftige Verwendung reserviert.
Ausgaben
Ihr Callback gibt ein Objekt mit zwei Kategorien von Feldern zurück:
- Top-Level-Felder funktionieren bei jedem Ereignis gleich:
systemMessagezeigt eine Nachricht für den Benutzer an, undcontinue(continue_in Python) bestimmt, ob der Agent nach diesem Hook weiterläuft. hookSpecificOutputsteuert die aktuelle Operation. Die Felder darin hängen vom Hook-Ereignistyp ab. FürPreToolUseHooks ist dies der Ort, an dem SiepermissionDecision("allow","deny","ask"oder"defer"),permissionDecisionReasonundupdatedInputsetzen. Wenn Sie"defer"zurückgeben, endet die Abfrage, damit Sie sie später fortsetzen können. FürPostToolUseHooks können SieadditionalContextsetzen, um Informationen zum Tool-Ergebnis anzuhängen, oderupdatedToolOutput, um die Ausgabe des Tools vollständig zu ersetzen, bevor Claude sie sieht.
Geben Sie {} zurück, um die Operation ohne Änderungen zuzulassen. SDK-Callback-Hooks verwenden das gleiche JSON-Ausgabeformat wie Claude Code Shell-Befehls-Hooks, das jedes Feld und ereignisspezifische Option dokumentiert. Für die SDK-Typdefinitionen siehe die TypeScript und Python SDK-Referenzen.
Wenn mehrere Hooks oder Berechtigungsregeln gelten, hat deny Vorrang vor defer, was Vorrang vor ask hat, was Vorrang vor allow hat. Wenn ein Hook deny zurückgibt, wird die Operation blockiert, unabhängig von anderen Hooks.
Asynchrone Ausgabe
Standardmäßig wartet der Agent darauf, dass Ihr Hook zurückkommt, bevor er fortfährt. Wenn Ihr Hook einen Nebeneffekt ausführt (Protokollierung, Webhook-Versand) und das Verhalten des Agenten nicht beeinflussen muss, können Sie stattdessen eine asynchrone Ausgabe zurückgeben. Dies teilt dem Agent mit, dass er sofort fortfahren soll, ohne auf die Fertigstellung des Hooks zu warten:
async def async_hook(input_data, tool_use_id, context):
# Start a background task, then return immediately
asyncio.create_task(send_to_logging_service(input_data))
return {"async_": True, "asyncTimeout": 30000}
const asyncHook: HookCallback = async (input, toolUseID, { signal }) => {
// Start a background task, then return immediately
sendToLoggingService(input).catch(console.error);
return { async: true, asyncTimeout: 30000 };
};
| Feld | Typ | Beschreibung |
|---|---|---|
async |
true |
Signalisiert Async-Modus. Der Agent fährt fort, ohne zu warten. In Python verwenden Sie async_, um das reservierte Schlüsselwort zu vermeiden. |
asyncTimeout |
number |
Optionales Timeout in Millisekunden für die Hintergrund-Operation |
Asynchrone Ausgaben können nicht blockieren, ändern oder Kontext in die Operation einfügen, da der Agent bereits weitergegangen ist. Verwenden Sie sie nur für Nebeneffekte wie Protokollierung, Metriken oder Benachrichtigungen.
Beispiele
Tool-Eingabe ändern
Dieses Beispiel fängt Write-Tool-Aufrufe ab und schreibt das file_path Argument um, um /sandbox voranzustellen, wodurch alle Datei-Schreibvorgänge in ein Sandbox-Verzeichnis umgeleitet werden. Der Callback gibt updatedInput mit dem geänderten Pfad und permissionDecision: 'allow' zurück, um die umgeschriebene Operation automatisch zu genehmigen:
async def redirect_to_sandbox(input_data, tool_use_id, context):
if input_data["hook_event_name"] != "PreToolUse":
return {}
if input_data["tool_name"] == "Write":
original_path = input_data["tool_input"].get("file_path", "")
return {
"hookSpecificOutput": {
"hookEventName": input_data["hook_event_name"],
"permissionDecision": "allow",
"updatedInput": {
**input_data["tool_input"],
"file_path": f"/sandbox{original_path}",
},
}
}
return {}
const redirectToSandbox: HookCallback = async (input, toolUseID, { signal }) => {
if (input.hook_event_name !== "PreToolUse") return {};
const preInput = input as PreToolUseHookInput;
const toolInput = preInput.tool_input as Record<string, unknown>;
if (preInput.tool_name === "Write") {
const originalPath = toolInput.file_path as string;
return {
hookSpecificOutput: {
hookEventName: preInput.hook_event_name,
permissionDecision: "allow",
updatedInput: {
...toolInput,
file_path: `/sandbox${originalPath}`
}
}
};
}
return {};
};
Wenn Sie updatedInput verwenden, müssen Sie auch permissionDecision: 'allow' einschließen, um die geänderte Eingabe automatisch zu genehmigen, oder permissionDecision: 'ask', um sie dem Benutzer anzuzeigen. Mit 'defer' wird updatedInput ignoriert. Geben Sie immer ein neues Objekt zurück, anstatt das ursprüngliche tool_input zu mutieren.
Kontext hinzufügen und ein Tool blockieren
Dieses Beispiel blockiert Schreibvorgänge in das /etc Verzeichnis und erklärt den Grund sowohl dem Modell als auch dem Benutzer:
permissionDecision: 'deny'stoppt den Tool-Aufruf.permissionDecisionReasonteilt dem Modell mit, warum, damit es nicht erneut versucht.systemMessagezeigt dem Benutzer, was passiert ist.
async def block_etc_writes(input_data, tool_use_id, context):
file_path = input_data["tool_input"].get("file_path", "")
if file_path.startswith("/etc"):
return {
# Top-level field: message shown to the user
"systemMessage": "Remember: system directories like /etc are protected.",
# hookSpecificOutput: block the operation
"hookSpecificOutput": {
"hookEventName": input_data["hook_event_name"],
"permissionDecision": "deny",
"permissionDecisionReason": "Writing to /etc is not allowed",
},
}
return {}
const blockEtcWrites: HookCallback = async (input, toolUseID, { signal }) => {
const preInput = input as PreToolUseHookInput;
const toolInput = preInput.tool_input as Record<string, unknown>;
const filePath = toolInput?.file_path as string;
if (filePath?.startsWith("/etc")) {
return {
// Top-level field: message shown to the user
systemMessage: "Remember: system directories like /etc are protected.",
// hookSpecificOutput: block the operation
hookSpecificOutput: {
hookEventName: preInput.hook_event_name,
permissionDecision: "deny",
permissionDecisionReason: "Writing to /etc is not allowed"
}
};
}
return {};
};
Spezifische Tools automatisch genehmigen
Standardmäßig kann der Agent vor der Verwendung bestimmter Tools um Genehmigung bitten. Dieses Beispiel genehmigt schreibgeschützte Dateisystem-Tools (Read, Glob, Grep) automatisch, indem permissionDecision: 'allow' zurückgegeben wird, sodass sie ohne Benutzerbestätigung ausgeführt werden, während alle anderen Tools normalen Berechtigungsprüfungen unterliegen:
async def auto_approve_read_only(input_data, tool_use_id, context):
if input_data["hook_event_name"] != "PreToolUse":
return {}
read_only_tools = ["Read", "Glob", "Grep"]
if input_data["tool_name"] in read_only_tools:
return {
"hookSpecificOutput": {
"hookEventName": input_data["hook_event_name"],
"permissionDecision": "allow",
"permissionDecisionReason": "Read-only tool auto-approved",
}
}
return {}
const autoApproveReadOnly: HookCallback = async (input, toolUseID, { signal }) => {
if (input.hook_event_name !== "PreToolUse") return {};
const preInput = input as PreToolUseHookInput;
const readOnlyTools = ["Read", "Glob", "Grep"];
if (readOnlyTools.includes(preInput.tool_name)) {
return {
hookSpecificOutput: {
hookEventName: preInput.hook_event_name,
permissionDecision: "allow",
permissionDecisionReason: "Read-only tool auto-approved"
}
};
}
return {};
};
Mehrere Hooks registrieren
Wenn ein Ereignis ausgelöst wird, werden alle übereinstimmenden Hooks parallel ausgeführt. Bei Berechtigungsentscheidungen gewinnt das restriktivste Ergebnis: Ein einzelnes deny blockiert den Tool-Aufruf, unabhängig davon, was die anderen Hooks zurückgeben. Da die Abschlussreihenfolge nicht deterministisch ist, schreiben Sie jeden Hook so, dass er unabhängig agiert, anstatt sich darauf zu verlassen, dass ein anderer Hook zuerst ausgeführt wurde.
Das folgende Beispiel registriert drei unabhängige Prüfungen für jeden Tool-Aufruf:
options = ClaudeAgentOptions(
hooks={
"PreToolUse": [
HookMatcher(hooks=[authorization_check]),
HookMatcher(hooks=[input_validator]),
HookMatcher(hooks=[audit_logger]),
]
}
)
const options = {
hooks: {
PreToolUse: [
{ hooks: [authorizationCheck] },
{ hooks: [inputValidator] },
{ hooks: [auditLogger] }
]
}
};
Mit Regex-Matchern filtern
Verwenden Sie Regex-Muster, um mehrere Tools abzugleichen. Dieses Beispiel registriert drei Matcher mit unterschiedlichen Bereichen: Der erste löst file_security_hook nur für Datei-Änderungs-Tools aus, der zweite löst mcp_audit_hook für alle MCP-Tools aus (Tools, deren Namen mit mcp__ beginnen), und der dritte löst global_logger für jeden Tool-Aufruf unabhängig vom Namen aus:
options = ClaudeAgentOptions(
hooks={
"PreToolUse": [
# Match file modification tools
HookMatcher(matcher="Write|Edit|Delete", hooks=[file_security_hook]),
# Match all MCP tools
HookMatcher(matcher="^mcp__", hooks=[mcp_audit_hook]),
# Match everything (no matcher)
HookMatcher(hooks=[global_logger]),
]
}
)
const options = {
hooks: {
PreToolUse: [
// Match file modification tools
{ matcher: "Write|Edit|Delete", hooks: [fileSecurityHook] },
// Match all MCP tools
{ matcher: "^mcp__", hooks: [mcpAuditHook] },
// Match everything (no matcher)
{ hooks: [globalLogger] }
]
}
};
Subagent-Aktivität verfolgen
Verwenden Sie SubagentStop Hooks, um zu überwachen, wenn Subagents ihre Arbeit beenden. Siehe den vollständigen Eingabetyp in den TypeScript und Python SDK-Referenzen. Dieses Beispiel protokolliert eine Zusammenfassung jedes Mal, wenn ein Subagent abgeschlossen wird:
async def subagent_tracker(input_data, tool_use_id, context):
# Log subagent details when it finishes
print(f"[SUBAGENT] Completed: {input_data['agent_id']}")
print(f" Transcript: {input_data['agent_transcript_path']}")
print(f" Tool use ID: {tool_use_id}")
print(f" Stop hook active: {input_data.get('stop_hook_active')}")
return {}
options = ClaudeAgentOptions(
hooks={"SubagentStop": [HookMatcher(hooks=[subagent_tracker])]}
)
import { HookCallback, SubagentStopHookInput } from "@anthropic-ai/claude-agent-sdk";
const subagentTracker: HookCallback = async (input, toolUseID, { signal }) => {
// Cast to SubagentStopHookInput to access subagent-specific fields
const subInput = input as SubagentStopHookInput;
// Log subagent details when it finishes
console.log(`[SUBAGENT] Completed: ${subInput.agent_id}`);
console.log(` Transcript: ${subInput.agent_transcript_path}`);
console.log(` Tool use ID: ${toolUseID}`);
console.log(` Stop hook active: ${subInput.stop_hook_active}`);
return {};
};
const options = {
hooks: {
SubagentStop: [{ hooks: [subagentTracker] }]
}
};
HTTP-Anfragen von Hooks aus stellen
Hooks können asynchrone Operationen wie HTTP-Anfragen ausführen. Fangen Sie Fehler in Ihrem Hook ab, anstatt sie zu propagieren, da eine nicht behandelte Ausnahme den Agent unterbrechen kann.
Dieses Beispiel sendet einen Webhook nach jeder Tool-Fertigstellung und protokolliert, welches Tool ausgeführt wurde und wann. Der Hook fängt Fehler ab, sodass ein fehlgeschlagener Webhook den Agent nicht unterbricht:
import asyncio
import json
import urllib.request
from datetime import datetime
def _send_webhook(tool_name):
"""Synchronous helper that POSTs tool usage data to an external webhook."""
data = json.dumps(
{
"tool": tool_name,
"timestamp": datetime.now().isoformat(),
}
).encode()
req = urllib.request.Request(
"https://api.example.com/webhook",
data=data,
headers={"Content-Type": "application/json"},
method="POST",
)
urllib.request.urlopen(req)
async def webhook_notifier(input_data, tool_use_id, context):
# Only fire after a tool completes (PostToolUse), not before
if input_data["hook_event_name"] != "PostToolUse":
return {}
try:
# Run the blocking HTTP call in a thread to avoid blocking the event loop
await asyncio.to_thread(_send_webhook, input_data["tool_name"])
except Exception as e:
# Log the error but don't raise. A failed webhook shouldn't stop the agent
print(f"Webhook request failed: {e}")
return {}
import { query, HookCallback, PostToolUseHookInput } from "@anthropic-ai/claude-agent-sdk";
const webhookNotifier: HookCallback = async (input, toolUseID, { signal }) => {
// Only fire after a tool completes (PostToolUse), not before
if (input.hook_event_name !== "PostToolUse") return {};
try {
await fetch("https://api.example.com/webhook", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
tool: (input as PostToolUseHookInput).tool_name,
timestamp: new Date().toISOString()
}),
// Pass signal so the request cancels if the hook times out
signal
});
} catch (error) {
// Handle cancellation separately from other errors
if (error instanceof Error && error.name === "AbortError") {
console.log("Webhook request cancelled");
}
// Don't re-throw. A failed webhook shouldn't stop the agent
}
return {};
};
// Register as a PostToolUse hook
for await (const message of query({
prompt: "Refactor the auth module",
options: {
hooks: {
PostToolUse: [{ hooks: [webhookNotifier] }]
}
}
})) {
console.log(message);
}
Benachrichtigungen an Slack weiterleiten
Verwenden Sie Notification Hooks, um Systembenachrichtigungen vom Agent zu empfangen und sie an externe Dienste weiterzuleiten. Benachrichtigungen werden für bestimmte Ereignistypen ausgelöst: permission_prompt (Claude benötigt Genehmigung), idle_prompt (Claude wartet auf Eingabe), auth_success (Authentifizierung abgeschlossen), elicitation_dialog (Claude fordert den Benutzer auf), elicitation_response (der Benutzer hat auf eine Elicitation geantwortet) und elicitation_complete (eine Elicitation wurde geschlossen). Jede Benachrichtigung enthält ein message Feld mit einer für Menschen lesbaren Beschreibung und optional einen title.
Dieses Beispiel leitet jede Benachrichtigung an einen Slack-Kanal weiter. Es erfordert eine Slack Incoming Webhook URL, die Sie erstellen, indem Sie eine App zu Ihrem Slack-Workspace hinzufügen und Incoming Webhooks aktivieren:
import asyncio
import json
import urllib.request
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, HookMatcher
def _send_slack_notification(message):
"""Synchronous helper that sends a message to Slack via incoming webhook."""
data = json.dumps({"text": f"Agent status: {message}"}).encode()
req = urllib.request.Request(
"https://hooks.slack.com/services/YOUR/WEBHOOK/URL",
data=data,
headers={"Content-Type": "application/json"},
method="POST",
)
urllib.request.urlopen(req)
async def notification_handler(input_data, tool_use_id, context):
try:
# Run the blocking HTTP call in a thread to avoid blocking the event loop
await asyncio.to_thread(_send_slack_notification, input_data.get("message", ""))
except Exception as e:
print(f"Failed to send notification: {e}")
# Return empty object. Notification hooks don't modify agent behavior
return {}
async def main():
options = ClaudeAgentOptions(
hooks={
# Register the hook for Notification events (no matcher needed)
"Notification": [HookMatcher(hooks=[notification_handler])],
},
)
async with ClaudeSDKClient(options=options) as client:
await client.query("Analyze this codebase")
async for message in client.receive_response():
print(message)
asyncio.run(main())
import { query, HookCallback, NotificationHookInput } from "@anthropic-ai/claude-agent-sdk";
// Define a hook callback that sends notifications to Slack
const notificationHandler: HookCallback = async (input, toolUseID, { signal }) => {
// Cast to NotificationHookInput to access the message field
const notification = input as NotificationHookInput;
try {
// POST the notification message to a Slack incoming webhook
await fetch("https://hooks.slack.com/services/YOUR/WEBHOOK/URL", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: `Agent status: ${notification.message}`
}),
// Pass signal so the request cancels if the hook times out
signal
});
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
console.log("Notification cancelled");
} else {
console.error("Failed to send notification:", error);
}
}
// Return empty object. Notification hooks don't modify agent behavior
return {};
};
// Register the hook for Notification events (no matcher needed)
for await (const message of query({
prompt: "Analyze this codebase",
options: {
hooks: {
Notification: [{ hooks: [notificationHandler] }]
}
}
})) {
console.log(message);
}
Häufige Probleme beheben
Hook wird nicht ausgelöst
- Überprüfen Sie, ob der Hook-Ereignisname korrekt und case-sensitiv ist (
PreToolUse, nichtpreToolUse) - Überprüfen Sie, ob Ihr Matcher-Muster den Tool-Namen genau abgleicht
- Stellen Sie sicher, dass der Hook unter dem richtigen Ereignistyp in
options.hooksist - Für Nicht-Tool-Hooks wie
StopundSubagentStopgleichen Matcher gegen verschiedene Felder ab (siehe Matcher-Muster) - Hooks werden möglicherweise nicht ausgelöst, wenn der Agent das
max_turnsLimit erreicht, da die Sitzung endet, bevor Hooks ausgeführt werden können
Matcher filtert nicht wie erwartet
Matcher gleichen nur Tool-Namen ab, nicht Dateipfade oder andere Argumente. Um nach Dateipfad zu filtern, prüfen Sie tool_input.file_path in Ihrem Hook:
const myHook: HookCallback = async (input, toolUseID, { signal }) => {
const preInput = input as PreToolUseHookInput;
const toolInput = preInput.tool_input as Record<string, unknown>;
const filePath = toolInput?.file_path as string;
if (!filePath?.endsWith(".md")) return {}; // Skip non-markdown files
// Process markdown files...
return {};
};
Hook-Timeout
- Erhöhen Sie den
timeoutWert in derHookMatcherKonfiguration - Verwenden Sie das
AbortSignalaus dem dritten Callback-Argument, um Abbruch elegant in TypeScript zu behandeln
Tool wird unerwartet blockiert
- Überprüfen Sie alle
PreToolUseHooks aufpermissionDecision: 'deny'Rückgaben - Fügen Sie Protokollierung zu Ihren Hooks hinzu, um zu sehen, welche
permissionDecisionReasonsie zurückgeben - Überprüfen Sie, ob Matcher-Muster nicht zu breit sind (ein leerer Matcher gleicht alle Tools ab)
Geänderte Eingabe wird nicht angewendet
-
Stellen Sie sicher, dass
updatedInputinhookSpecificOutputist, nicht auf der obersten Ebene:return { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { command: "new command" } } }; -
Sie müssen auch
permissionDecision: 'allow'oder'ask'zurückgeben, damit die Eingabeänderung wirksam wird -
Schließen Sie
hookEventNameinhookSpecificOutputein, um zu identifizieren, für welchen Hook-Typ die Ausgabe bestimmt ist
Sitzungs-Hooks nicht in Python verfügbar
SessionStart und SessionEnd können als SDK-Callback-Hooks in TypeScript registriert werden, sind aber im Python SDK nicht verfügbar (HookEvent lässt sie weg). In Python sind sie nur als Shell-Befehls-Hooks verfügbar, die in Einstellungsdateien definiert sind (z. B. .claude/settings.json). Um Shell-Befehls-Hooks aus Ihrer SDK-Anwendung zu laden, schließen Sie die entsprechende Einstellungsquelle mit setting_sources oder settingSources ein:
options = ClaudeAgentOptions(
setting_sources=["project"], # Loads .claude/settings.json including hooks
)
const options = {
settingSources: ["project"] // Loads .claude/settings.json including hooks
};
Um stattdessen Initialisierungslogik als Python SDK-Callback auszuführen, verwenden Sie die erste Nachricht von client.receive_response() als Auslöser.
Subagent-Berechtigungsaufforderungen vervielfachen sich
Beim Spawnen mehrerer Subagents kann jeder einzelne Berechtigungen separat anfordern. Subagents erben nicht automatisch Berechtigungen des übergeordneten Agenten. Um wiederholte Aufforderungen zu vermeiden, verwenden Sie PreToolUse Hooks, um spezifische Tools automatisch zu genehmigen, oder konfigurieren Sie Berechtigungsregeln, die für Subagent-Sitzungen gelten.
Rekursive Hook-Schleifen mit Subagents
Ein UserPromptSubmit Hook, der Subagents spawnt, kann unendliche Schleifen erzeugen, wenn diese Subagents denselben Hook auslösen. Um dies zu verhindern:
- Überprüfen Sie auf einen Subagent-Indikator in der Hook-Eingabe, bevor Sie spawnen
- Verwenden Sie eine gemeinsame Variable oder Sitzungsstatus, um zu verfolgen, ob Sie bereits in einem Subagent sind
- Beschränken Sie Hooks so, dass sie nur für die Top-Level-Agent-Sitzung ausgeführt werden
systemMessage wird nicht in der Ausgabe angezeigt
Das systemMessage Feld zeigt eine Nachricht für den Benutzer an, nicht für das Modell. Standardmäßig gibt das SDK Hook-Ausgaben nicht im Nachrichtenstrom aus, daher wird die Nachricht möglicherweise nicht angezeigt, es sei denn, Sie setzen includeHookEvents (include_hook_events in Python). Um stattdessen Kontext an das Modell zu übergeben, geben Sie additionalContext zurück.
Wenn Sie Hook-Entscheidungen für Ihre Anwendung zuverlässig sichtbar machen müssen, protokollieren Sie sie separat oder verwenden Sie einen dedizierten Ausgabekanal.
Verwandte Ressourcen
- Claude Code Hooks-Referenz: vollständige JSON-Eingabe-/Ausgabeschemas, Ereignisdokumentation und Matcher-Muster
- Claude Code Hooks-Leitfaden: Shell-Befehls-Hook-Beispiele und Walkthroughs
- TypeScript SDK-Referenz: Hook-Typen, Eingabe-/Ausgabedefinitionen und Konfigurationsoptionen
- Python SDK-Referenz: Hook-Typen, Eingabe-/Ausgabedefinitionen und Konfigurationsoptionen
- Berechtigungen: Steuern Sie, was Ihr Agent tun kann
- Benutzerdefinierte Tools: Erstellen Sie Tools, um Agent-Funktionen zu erweitern