MCP Tool Hooks in Claude Code
Wie du MCP-Server-Tools direkt aus Claude Code Hooks aufrufst mit type: mcp_tool — Schema, Substitutions-Syntax, Anwendungsfälle und Produktionsmuster.
Hören Sie auf zu konfigurieren. Fangen Sie an zu bauen.
SaaS-Builder-Vorlagen mit KI-Orchestrierung.
Problem: Deine Hooks laufen als Shell-Skripte. Jedes Mal, wenn ein Hook einen MCP-Server aufrufen muss, startet er einen Subprozess, richtet den Transport ein, handhabt Auth, parsed die Antwort und formatiert JSON-Output zurück an stdout. Für einen Formatter oder einen Security-Check, der bei jedem File-Write feuert, summiert sich das.
Quick Win: Ab v2.1.118 haben Hooks einen neuen Typ, der MCP-Tools direkt aufruft. Füge das zu .claude/settings.json hinzu, um nach jedem File-Write einen Security-Scan zu starten, ganz ohne Subprozess:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "mcp_tool",
"server": "semgrep",
"tool": "scan_file",
"input": { "path": "${tool_input.file_path}" }
}
]
}
]
}
}Der MCP-Server läuft bereits. Der Hook überspringt die Shell komplett und ruft direkt die RPC-Verbindung des Servers auf. Der Text-Output des Tools läuft durch denselben JSON-Decision-Parser wie jeder Command-Hook.
Was type: "mcp_tool" eigentlich ist
Vor v2.1.118 gab es vier Handler-Typen für Hooks: command, http, prompt und agent. Jetzt sind es fünf:
| Typ | Was läuft |
|---|---|
command | Shell-Subprozess (stdin/stdout) |
http | POST an einen URL-Endpunkt |
mcp_tool | Direkter RPC-Call an einen verbundenen MCP-Server |
prompt | Single-Turn LLM-Auswertung (Haiku als Standard) |
agent | Multi-Turn-Subagent mit Read/Grep/Glob-Zugriff |
mcp_tool funktioniert bei jedem Hook-Event, genau wie command und http. Ein praktischer Vorbehalt: SessionStart und Setup feuern, während Server noch verbinden, daher kann es beim ersten Lauf zu einem "server not connected"-Fehler kommen. Folgeläufe funktionieren problemlos.
Das vollständige Schema
Drei Felder sind spezifisch für mcp_tool-Hooks. Der Rest wird mit allen Hook-Typen geteilt:
{
"type": "mcp_tool",
"server": "my-mcp-server",
"tool": "tool_name",
"input": {
"arg1": "${tool_input.file_path}",
"arg2": "${session_id}"
},
"timeout": 30,
"statusMessage": "Checking...",
"if": "Edit(*.ts|*.tsx)"
}| Feld | Pflicht | Beschreibung |
|---|---|---|
server | JA | Exakter Name des MCP-Servers, wie in den Settings konfiguriert |
tool | JA | Tool-Name auf diesem Server |
input | nein | Argumente, die an das Tool übergeben werden. Unterstützt ${path}-Substitution |
timeout | nein | Sekunden, bevor der Hook abgebrochen wird |
statusMessage | nein | Spinner-Text, der während der Hook läuft angezeigt wird |
if | nein | Permission-Rule-Syntax-Filter. Hook feuert nur, wenn der vollständige Call übereinstimmt |
Wichtig: server muss exakt mit dem Server-Namen in deiner MCP-Konfiguration übereinstimmen. Ein einziges falsches Zeichen und der Hook schlägt lautlos mit einem nicht-blockierenden Fehler fehl.
Input-Substitution
String-Werte in input unterstützen ${field.path}-Dot-Notation in das vollständige Event-JSON des Hooks. Für einen PostToolUse-Hook bei einem Write-Call sieht das Event-JSON so aus:
{
"session_id": "abc123",
"cwd": "/your/project",
"hook_event_name": "PostToolUse",
"tool_name": "Write",
"tool_use_id": "toolu_01...",
"tool_input": {
"file_path": "/your/project/src/api.ts",
"content": "..."
},
"tool_response": { "filePath": "/your/project/src/api.ts", "success": true },
"duration_ms": 142
}"${tool_input.file_path}" löst sich also zu /your/project/src/api.ts auf. Jedes Feld in diesem Objekt ist erreichbar. Das duration_ms-Feld wurde in v2.1.119 hinzugefügt, eine Version nach mcp_tool.
Wie der Output verarbeitet wird
Der Text-Content des MCP-Tools wird genauso behandelt wie der stdout eines Command-Hooks. Wenn er als valides JSON geparst werden kann, handelt Claude Code entsprechend der Decision-Felder. Wenn nicht, wird der Text zu Kontext für Claude.
Die Decision-Felder funktionieren genauso wie bei jedem anderen Hook:
{
"decision": "block",
"reason": "Security issue found in src/api.ts: SQL injection risk on line 42."
}Gibt ein PostToolUse-MCP-Tool-Hook das zurück, bekommt Claude die Meldung und korrigiert die Datei. Das Tool hat schon ausgeführt, daher ist das beratend, nicht präventiv. Für echtes Blockieren vor einem Tool-Run nutze PreToolUse und gib permissionDecision: "deny" zurück.
Ein Feld ist exklusiv für mcp_tool-Hooks bei PostToolUse: updatedMCPToolOutput. Es ersetzt, was Claude als Output des Tools sieht, bevor es in die Konversation eingeht. Ein laufender MCP-Server kann das Ergebnis eines anderen Tools nachbearbeiten, bevor Claude es liest.
Warum das besser ist als Shell-Command-Hooks
Zwei konkrete Unterschiede, nicht nur Geschwindigkeit.
Stateful Server. Ein Shell-Subprozess startet jedes Mal neu. Ein MCP-Server ist ein lebendiger Prozess mit eigenem Zustand: geladene Configs, offene Verbindungen, Caches, akkumulierter Session-Kontext. Ein Linting-MCP, das dein tsconfig.json beim Start geparst hat, parst es nicht bei jedem File-Write neu. Ein Command-Hook schon.
Keine Shell-Umgebungs-Abhängigkeit. Command-Hooks schlagen lautlos fehl, wenn PATH falsch ist, wenn jq nicht installiert ist, wenn ~/.zshrc etwas nach stdout ausgibt bei nicht-interaktiven Shells. MCP-Tool-Hooks umgehen das alles. Der Call geht direkt von Claude Code zum Server über die bestehende RPC-Verbindung.
Das if-Feld: Hooks eingrenzen
Ohne if feuert ein Hook bei jedem Event, das zum matcher passt. Mit if startet der Hook-Prozess nur, wenn der vollständige Tool-Call (Name und Argumente) der Permission-Rule-Syntax entspricht:
{
"type": "mcp_tool",
"server": "semgrep",
"tool": "scan_file",
"if": "Edit(*.py|*.ts|*.js)",
"input": { "path": "${tool_input.file_path}" }
}Dieser Hook läuft nie auf .md- oder .json-Dateien. Bei einem Projekt mit vielen Doku-Edits macht das einen echten Performance-Unterschied.
Muster 1: Security-Scan bei jedem Write
Ein Security-MCP-Server, der einen Dateipfad akzeptiert und Findings zurückgibt. Claude blockieren, wenn er etwas findet:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "mcp_tool",
"server": "semgrep",
"tool": "scan_file",
"if": "Write(*.ts|*.py|*.js|*.go)",
"input": { "path": "${tool_input.file_path}" },
"statusMessage": "Scanning..."
}
]
}
]
}
}Wenn das MCP-Tool ein Finding zurückgibt, strukturiere die Antwort so:
{
"decision": "block",
"reason": "Semgrep finding: [description of issue at line N]"
}Claude bekommt die Block-Meldung und überarbeitet die Datei. Der Scan läuft auf dem gecachten Ruleset des Servers, nicht auf einem frisch gestarteten Subprozess-Parse.
Muster 2: Stop-Hook mit externer Verifizierung
Ein Stop-Hook, der ein Linear- oder Jira-MCP aufruft, um zu prüfen, ob das zugehörige Ticket wirklich geschlossen ist, bevor Claude "fertig" meldet:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "mcp_tool",
"server": "linear",
"tool": "get_issue_status",
"input": { "issue_id": "${tool_input.issue_id}" }
}
]
}
]
}
}Das MCP-Tool gibt den Ticket-Status zurück. Kommt In Progress zurück, sollte das Response-JSON decision: "block" und einen Grund tragen. Claude arbeitet weiter.
Überprüfe immer stop_hook_active in deiner Stop-Hook-Logik. Das Event-JSON enthält dieses Feld als "true", wenn Claude bereits aus einem vorherigen Stop-Hook-Aufruf heraus weiterläuft. Ein Server, der das nicht prüft, erzeugt eine Endlosschleife. Baue die Absicherung direkt ins MCP-Tool ein: Wenn stop_hook_active im Input "true" ist, leere Ausgabe zurückgeben und sauber beenden.
Muster 3: Production-Error-Check vor dem Stoppen
Nachdem Claude ein Feature fertiggestellt hat, prüfen, ob in Staging etwas Neues kaputt gegangen ist, bevor die Session als abgeschlossen gilt. Ein Sentry-MCP übernimmt die Abfrage:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "mcp_tool",
"server": "sentry",
"tool": "get_new_errors_since",
"input": { "minutes": "5", "skip_if_active": "${stop_hook_active}" }
}
]
}
]
}
}Wenn in den letzten fünf Minuten neue Fehler aufgetaucht sind, gibt das MCP-Tool sie zusammen mit decision: "block" zurück. Claude liest die Fehlerdetails und behebt die Regression, bevor es stoppt.
Muster 4: Docs automatisch vor jedem Prompt laden
Ein UserPromptSubmit-Hook mit einem Context7-MCP holt aktuelle Dokumentation für jede im Prompt erwähnte Library, bevor Claude ihn verarbeitet:
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "mcp_tool",
"server": "context7",
"tool": "get_library_docs",
"input": { "prompt": "${prompt}" },
"timeout": 15
}
]
}
]
}
}Früher musste Claude das MCP-Tool explizit aufrufen. Jetzt passiert es bei jedem Prompt automatisch. Claude startet mit aktuellen Docs statt mit Trainingsdaten.
Muster 5: Policy-Enforcement für Agent-Teams
Bei Multi-Agent-Workflows kann ein gemeinsamer Policy-MCP-Server durchsetzen, welcher Agent in welche Verzeichnisse schreiben darf. Die Umgebungsvariable CLAUDE_AGENT_NAME identifiziert den aktuellen Agenten. Ein PreToolUse-Hook ruft den Policy-Server auf:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "mcp_tool",
"server": "policy-server",
"tool": "check_write_permission",
"input": {
"agent": "${agent_name}",
"path": "${tool_input.file_path}"
}
}
]
}
]
}
}Der Policy-Server hält die vollständige Autorisierungs-Map. Den Server einmal aktualisieren und jeder Agent in jedem Projekt erbt die neuen Regeln, ohne eine einzige settings.json anzufassen.
Muster 6: MCP-Tool-Hooks im Agent-Frontmatter
Hooks müssen nicht in settings.json leben. Sie können im YAML-Frontmatter eines Agenten stehen, auf den Lebenszyklus dieses Agenten beschränkt:
---
name: backend-developer
description: Builds API endpoints and database logic
hooks:
PostToolUse:
- matcher: "Write"
hooks:
- type: mcp_tool
server: semgrep
tool: scan_file
input: { "path": "${tool_input.file_path}" }
Stop:
- hooks:
- type: agent
prompt: "Verify all API endpoints have corresponding tests. Block if any are missing."
---Jeder Spezialist-Agent in einem orchestrierten Team trägt seine eigene Validierungslogik. Der Backend-Agent scannt auf Security-Probleme. Der Frontend-Agent prüft Accessibility. Keiner braucht einen globalen Hook, der für alle gilt.
Elicitation-Steuerung
Das Elicitation-Event feuert, wenn ein MCP-Server während einer Aufgabe Nutzereingaben anfordert. Ein mcp_tool-Hook kann bekannte Prompts automatisch beantworten, indem er einen Secrets-Manager aufruft:
{
"hooks": {
"Elicitation": [
{
"matcher": "my-db-server",
"hooks": [
{
"type": "mcp_tool",
"server": "secrets-manager",
"tool": "get_credential",
"input": { "key": "${elicitation.field_name}" }
}
]
}
]
}
}Vorhersehbare Credential-Prompts lösen sich automatisch auf. Die Aufgabe läuft ohne Unterbrechung durch.
MCP-Server, die gut zu Hooks passen
Nicht jeder MCP-Server eignet sich als Hook-Ziel. Am besten geeignet sind Tools, die bei bestimmten Events ohne User-Eingriff feuern müssen:
| Server | Event | Was er tut |
|---|---|---|
| Semgrep | PostToolUse: Write | Security-Scan bei jedem Write |
| Sentry | Stop | Neue Staging-Fehler prüfen vor dem Abschluss |
| Linear / Jira | Stop, TaskCompleted | Ticket-Status prüfen, bei Abschluss aktualisieren |
| Context7 | UserPromptSubmit | Aktuelle Docs für erwähnte Libraries automatisch laden |
| ElevenLabs | Stop, Notification | TTS-Audio bei Aufgabenabschluss |
| Slack | Notification, Stop | Team-Alerts ohne curl-Boilerplate |
| E2B | Stop | Generierte Skripte in einer Sandbox ausführen vor dem Abschluss |
| claude-mem | PostCompact, SessionStart | Session-Kontext nach Compaction wiederherstellen |
| n8n | TaskCompleted | Externen Workflow bei Abschluss triggern |
Bekanntes Problem: PostToolUse + MCP-Events + additionalContext
Es gibt einen offenen Bug (GitHub Issue #24788), bei dem additionalContext von Hooks stillschweigend verworfen wird, wenn das auslösende Event ein MCP-Tool-Call war. Das betrifft type: "command"-Hooks, die auf MCP-Tool-Events reagieren, nicht mcp_tool-Hooks selbst.
Der Unterschied ist wichtig: Hooks, die MCP-Aufrufe SIND, funktionieren einwandfrei. Hooks, die AUF MCP-Tool-Calls REAGIEREN und additionalContext zurückgeben, tun es nicht. Der Workaround ist exit 2 plus stderr für kritische Meldungen aus PostToolUse-Hooks, die auf MCP-Tool-Calls zielen. Das Blocking-Pattern funktioniert; Advisory-Injection tut es nicht.
MCP-Tool-Hooks sind das letzte fehlende Stück
Vorher waren Hooks ein Sicherheitsnetz. Shell-Befehle, die gefährliche Dinge blockieren oder Formatter ausführen konnten. Zustandslos, prozesslokal, von allem getrennt, was deine MCP-Server bereits wissen.
Danach: Hooks sind eine deterministische Orchestrierungsschicht. Jedes Event, jedes MCP-Tool, volle Decision-Kontrolle, mit Zustand, der über Calls hinweg erhalten bleibt, und kein Subprozess-Overhead.
Die Pipeline ist jetzt vollständig. PreToolUse validiert. PostToolUse formatiert und scannt. PostToolBatch führt Tests aus. Stop verifiziert mit echten externen Daten. Jeder Schritt kann ein MCP-Tool-Aufruf sein, und keiner davon braucht ein Shell-Skript.
Hören Sie auf zu konfigurieren. Fangen Sie an zu bauen.
SaaS-Builder-Vorlagen mit KI-Orchestrierung.