ai newsMo., 2. Nov.
EilmeldungMeituan's LongCat-2.5 Drops Into Claude Code at $0.30/$1.20. Its Setup Caps You at 200K

Claude Code Hooks: PreToolUse, PostToolUse und Stop mit Beispielen erklärt

Hooks sind Befehle, die Claude Code an festen Punkten seines Lebenszyklus ausführt, etwa vor oder nach einem Tool-Aufruf. Ein Artikel erklärt die Funktionsweise, das JSON-Format und fünf anpassbare Beispiele.

dev.toMontag 14:49 · 02.11.DE9 Min. Lesezeit
Bild: dev.to

Veröffentlicht am 2. November • Ursprünglich veröffentlicht auf learn.homingo.co.in • Fully Autonomous

CLAUDE.md bittet; ein hook stellt sicher. Wie hooks funktionieren, welches JSON sie lesen und zurückgeben, und fünf Beispiele zum direkten Anpassen.

Ein hook ist ein Befehl, den Claude Code an einem festen Punkt seines Lebenszyklus ausführt: vor einem Tool-Aufruf (PreToolUse), nachdem einer erfolgreich war (PostToolUse), wenn Claude seine Antwort abschließt (Stop), wenn eine Session startet und vieles mehr. Anweisungen in CLAUDE.md sind Kontext, den Claude zu befolgen versucht; ein hook läuft jedes Mal, egal was das Modell entscheidet. Nutzen Sie CLAUDE.md für Urteilsvermögen und hooks für alles, was immer geschehen oder nie geschehen darf.

Hooks gehören in eine Settings-Datei: ~/.claude/settings.json für alle Ihre Projekte,.claude/settings.json für ein einzelnes Projekt (committen Sie sie, um sie mit dem Team zu teilen) oder.claude/settings.local.json nur für sich selbst. Subagents und Skills können ebenfalls hooks in ihrem Frontmatter mitbringen, die nur aktiv sind, solange sie laufen. Die Struktur hat drei Ebenen: das Event, eine Matcher-Gruppe und die auszuführenden Befehle.

{ "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write", "timeout": 30 } ] } ] } } Dieses ist das offizielle Auto-Format-Beispiel: nach jedem Edit oder Write formatiert Prettier die Datei, die Claude angefasst hat. Der Matcher ist ein Tool-Name, eine Liste wie Edit|Write oder ein regulärer Ausdruck (mcp__github__.* passt auf jedes Tool eines MCP-Servers namens github); * oder ein leerer Matcher bedeutet jedes Tool. Geben Sie /hooks in einer Session ein, um zu sehen, was konfiguriert ist und woher jeder hook kommt. Dieses Beispiel benötigt jq und scheitert an Pfaden mit Leerzeichen; die Python-Hooks unten vermeiden beides.

Claude Code sendet das Event als JSON an den Standard-Input des hooks. Bei Tool-Events enthält es session_id, cwd, hook_event_name, tool_name und tool_input (bei Edit und Write ist tool_input.file_path immer absolut); PostToolUse ergänzt tool_response. Der hook antwortet auf eine von zwei Arten:

Hooks ändern sich schnell zwischen den Claude-Code-Versionen. Alles hier folgt der offiziellen hooks-Referenz von November 2026; prüfen Sie diese, bevor Sie sich auf ein selteneres Event oder Feld verlassen. Wir schreiben die folgenden Skripte in Python, damit sie auf macOS, Linux und Windows gleich laufen, ohne jq.

import json import sys from pathlib import PurePosixPath data = json. load ( sys. stdin ) path = PurePosixPath ( data. get ( " tool_input ", {}). get ( " file_path ", "" ). replace ( " \\ ", " / " )) name, parts = path. name, set ( path. parts ) blocked = ( ( name == ".env " or ( name. startswith ( ".env. " ) and name!= ".env.example " )) or name == " package-lock.json " or ".git " in parts or " secrets " in parts ) if blocked: print ( f " Blocked: { path } is protected. Ask the user to change it. ", file = sys. stderr ) sys. exit ( 2 ) # exit 2 = block the call; stderr goes to Claude as the reason sys. exit ( 0 ).claude/settings.json

{ "hooks": { "PreToolUse": [ { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "python \" $CLAUDE_PROJECT_DIR/.claude/hooks/protect_files.py \" " }] } ] } } $CLAUDE_PROJECT_DIR ist der Projekt-Root, sodass der hook funktioniert, in welchem Ordner Claude sich auch befindet. Verwenden Sie python3 auf macOS oder Linux, wenn Ihr Interpreter so heißt, oder py unter Windows. Falls Ihre Settings-Datei bereits ein "hooks"-Objekt enthält, fügen Sie das Event darin ein, statt einen zweiten "hooks"-Key anzulegen.

Testen Sie es, indem Sie Claude bitten, einen Kommentar zu.env hinzuzufügen: die Bearbeitung wird verweigert und Claude sieht die Blocked:-Zeile. Ein hook schützt nur die Tools, auf die er passt, und dieser hier stoppt nur Bearbeitungen. Fügen Sie Read(./.env) und Edit(./.env) (und dasselbe für./.secrets/**) zu permissions.deny in Ihren Settings hinzu, damit Claude die Secrets weder in seinen Kontext lesen noch mit seinen Datei-Tools ändern kann.

Bash bleibt die Lücke: verengen Sie sie mit Bash-Deny-Regeln und Claude Codes Bash-Sandboxing (/sandbox).

In unserem Subagents-Guide misst ein file-qa-Agent ein gerendertes Vertical-Video mit ffprobe. Sein Prompt sagt "nur ffprobe ausführen", aber ein Prompt ist eine Bitte. Ein hook im eigenen Frontmatter des Agents macht daraus eine Regel, die nur aktiv ist, solange dieser Agent läuft:

--- name: file-qa description: " Technical QA of a rendered vertical video file. Use after every render, before posting." tools: Bash model: haiku hooks: PreToolUse: - matcher: " Bash" hooks: - type: command command: ' python "$CLAUDE_PROJECT_DIR/.claude/hooks/only_ffprobe.py"' ---.claude/hooks/only_ffprobe.py

import json import shlex import sys data = json. load ( sys. stdin ) command = data. get ( " tool_input ", {}). get ( " command ", "" ) def decide ( decision, reason ): print ( json. dumps ({ " hookSpecificOutput ": { " hookEventName ": " PreToolUse ", " permissionDecision ": decision, " permissionDecisionReason ": reason, }})) sys. exit ( 0 ) if any ( ch in command for ch in ";&|<>`$ \n\r {}*?[]~#() " ): decide ( " deny ", " One plain ffprobe command only: no pipes, redirects, chaining, substitutions, globs or braces. " ) try: args = shlex. split ( command ) except ValueError: decide ( " deny ", " Could not parse the command. " ) if not args or args [ 0 ]!= " ffprobe ": decide ( " deny ", " This agent may run ffprobe only.

" ) if any ( a in ( " -o ", " -report " ) for a in args [ 1:]): decide ( " deny ", " ffprobe may not write files here (-o and -report are blocked). " ) decide ( " allow ", " Plain ffprobe probe. " ) Shell-Metazeichen zuerst abzulehnen ist wichtig: ffprobe x.mp4; rm -rf renders beginnt ebenfalls mit ffprobe.

Das gilt auch für ffprobe -o, das seine Ausgabe in eine Datei schreibt, daher die letzte Prüfung. Geschweifte Klammern sind ebenfalls relevant: ffprobe x.mp4 {-o,out.txt} expandiert zu -o out.txt, bevor ffprobe läuft, deshalb werden sie zusammen mit den anderen Shell-Zeichen verweigert. Ein deny durch einen hook gilt in jedem Berechtigungsmodus, sogar in bypassPermissions.

Ein allow überspringt die Berechtigungsabfrage, überschreibt aber nie eine deny-Regel in Ihren Settings.

import json import os import subprocess import sys import tempfile from pathlib import Path MAX_TRIES = 3 data = json. load ( sys. stdin ) counter = Path ( tempfile. gettempdir ()) / f " claude-stop- { data. get ( ' session_id ', ' default ' ) }.txt " tries = int ( counter. read_text ()) if counter. exists () else 0 project = os. environ. get ( " CLAUDE_PROJECT_DIR " ) or data. get ( " cwd " ) result = subprocess. run ( " npm test --silent ", shell = True, capture_output = True, text = True, cwd = project, env = { ** os. environ, " CI ": " true " }, # CI=true keeps test runners out of watch mode ) if result. returncode == 0 or tries >= MAX_TRIES: counter. unlink ( missing_ok = True ) # tests pass, or out of tries: let Claude stop sys. exit ( 0 ) counter. write_text ( str ( tries + 1 )) tail = ( result. stdout + result. stderr )[ - 1500:] reason = f " Tests fail (try { tries + 1 } of { MAX_TRIES } ). Fix them before finishing: \n { tail } " print ( json. dumps ({ " decision ": " block ", " reason ": reason })) Wenn die Suite fehlschlägt, erhält Claude die Fehlerausgabe als nächste Anweisung, statt den Zug zu beenden, bis zu dreimal hintereinander; danach darf Claude stoppen, der Zähler wird zurückgesetzt und Sie entscheiden. Die offizielle Dokumentation empfiehlt, das Eingabefeld stop_hook_active zu prüfen, um Endlosschleifen zu vermeiden, das erlaubt aber nur einen Wiederholungsversuch; ein kleiner Zähler erlaubt einige.

Claude Code überschreibt außerdem einen Stop-hook, der achtmal hintereinander blockiert, ohne dass dazwischen ein Tool-Aufruf erfolgt. Stop wird ausgelöst, wann immer Claude eine Antwort abschließt, halten Sie also die Suite schnell oder führen Sie nur die Tests für die geänderten Teile aus. Registrieren Sie ihn mit einem Timeout, das länger ist als Ihre Suite braucht (in Sekunden; Stop benötigt keinen Matcher):

{ "hooks": { "Stop": [ { "hooks": [{ "type": "command", "command": "python \" $CLAUDE_PROJECT_DIR/.claude/hooks/tests_before_stop.py \" ", "timeout": 300 }] } ] } } Beispiel 4: Kontext hinzufügen, nachdem ein Tool gelaufen ist (PostToolUse). Ein PostToolUse-hook kann nichts rückgängig machen, aber er kann Claude etwas mitteilen. Dieser hier, registriert für Edit|Write, warnt Claude, wenn es eine generierte Datei bearbeitet, damit die nächste Änderung stattdessen in der Spezifikation landet:

{ "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "python \" $CLAUDE_PROJECT_DIR/.claude/hooks/generated_note.py \" " }] } ] } }.claude/hooks/generated_note.py

import json import sys data = json. load ( sys. stdin ) path = data. get ( " tool_input ", {}). get ( " file_path ", "" ). replace ( " \\ ", " / " ) if " /src/generated/ " in path: print ( json. dumps ({ " hookSpecificOutput ": { " hookEventName ": " PostToolUse ", " additionalContext ": " This file is generated from the episode spec and is overwritten on the next render. " " Make the change in episodes/<id>/spec.json instead. ", }})) Enter fullscreen mode Exit fullscreen mode Um zentrale Regeln nach der Kompaktierung der Konversation erneut einzuspeisen, kann ein SessionStart-Hook mit dem Matcher compact diese ausgeben; plain stdout von SessionStart gelangt direkt in den Kontext von Claude.

{ "hooks": { "Notification": [ { "hooks": [{ "type": "command", "command": "notify-send 'Claude Code' 'Claude Code needs your attention'" }] } ] } } Enter fullscreen mode Exit fullscreen mode Trage es in deine User-Einstellungen ein, damit es in jedem Projekt funktioniert. Auf macOS verwende als Befehl osascript -e 'display notification "Claude Code needs your attention" with title "Claude Code"'; in der offiziellen Hooks-Anleitung gibt es außerdem eine PowerShell-Version für Windows.

Hooks runden das Set ab: CLAUDE.md enthält die Projektregeln, ein Skill beschreibt, wie eine Aufgabe zu erledigen ist, Subagents prüfen das Ganze mit frischem Kontext, und Hooks erzwingen die Linien, die niemand überschreiten darf. Wenn du mit Claude Code kurze vertikale Videos erstellst, paketiert der folgende Skill den spec-first-Workflow, aus dem die Beispiele 2 und 4 stammen.

Animated horror reels Skill: Unser komplettes Playbook als Claude Code Agent Skill: die hook-first Story-Methode, die Episode-Spec-Vorlage, das Kamera-, Licht- und Grading-System mit einem Paletten-Builder, ein renderfertiger Remotion-Starter und die QA-Checks vor dem Posten. Erstelle Reels wie unsere. Sieh dir den Skill an

Hinweis: Dieser Guide stammt von Homingo, dem Team hinter dem oben verlinkten Skill. Alles darin funktioniert auch ohne ihn.

Automatisch übersetzt. Das Original auf Englisch ist unten aufklappbar.

Original (Englisch) anzeigen

Posted on Nov 2 • Originally published at learn.homingo.co.in • Fully Autonomous

CLAUDE.md asks; a hook makes sure. How hooks work, the JSON they read and return, and five examples to adapt today.

A hook is a command that Claude Code runs at a fixed point in its lifecycle: before a tool call ( PreToolUse ), after one succeeds ( PostToolUse ), when Claude finishes its reply ( Stop ), when a session starts, and many more. Instructions in CLAUDE.md are context that Claude tries to follow; a hook runs every time, whatever the model decides. Use CLAUDE.md for judgement and hooks for anything that must always happen or must never happen.

Hooks go in a settings file: ~/.claude/settings.json for all your projects,.claude/settings.json for one project (commit it to share with the team) or.claude/settings.local.json for yourself only. Subagents and skills can also carry hooks in their frontmatter, active only while they run. The structure has three levels: the event, a matcher group, and the commands to run.

{ "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write", "timeout": 30 } ] } ] } } Enter fullscreen mode Exit fullscreen mode That one is the official auto-format example: after every Edit or Write, Prettier formats the file Claude touched. The matcher is a tool name, a list like Edit|Write, or a regular expression ( mcp__github__.* matches every tool of an MCP server called github ); * or an empty matcher means every tool. Type /hooks in a session to see what is configured and where each hook comes from. This one needs jq and breaks on paths with spaces; the Python hooks below avoid both.

Claude Code sends the event as JSON on the hook’s standard input. For tool events it includes session_id, cwd, hook_event_name, tool_name and tool_input (for Edit and Write, tool_input.file_path is always absolute); PostToolUse adds tool_response. The hook answers in one of two ways:

Hooks change quickly between Claude Code versions. Everything here follows the official hooks reference as of November 2026; check it before relying on a rarer event or field. We write the scripts below in Python so they run the same on macOS, Linux and Windows, with no jq needed.

import json import sys from pathlib import PurePosixPath data = json. load ( sys. stdin ) path = PurePosixPath ( data. get ( " tool_input ", {}). get ( " file_path ", "" ). replace ( " \\ ", " / " )) name, parts = path. name, set ( path. parts ) blocked = ( ( name == ".env " or ( name. startswith ( ".env. " ) and name!= ".env.example " )) or name == " package-lock.json " or ".git " in parts or " secrets " in parts ) if blocked: print ( f " Blocked: { path } is protected. Ask the user to change it. ", file = sys. stderr ) sys. exit ( 2 ) # exit 2 = block the call; stderr goes to Claude as the reason sys. exit ( 0 ) Enter fullscreen mode Exit fullscreen mode.claude/settings.json

{ "hooks": { "PreToolUse": [ { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "python \" $CLAUDE_PROJECT_DIR/.claude/hooks/protect_files.py \" " }] } ] } } Enter fullscreen mode Exit fullscreen mode $CLAUDE_PROJECT_DIR is the project root, so the hook works whatever folder Claude is in. Use python3 on macOS or Linux if that is your interpreter’s name, or py on Windows. If your settings file already has a "hooks" object, add the event inside it rather than a second "hooks" key.

Test it by asking Claude to add a comment to.env: the edit is refused and Claude sees the Blocked: line. A hook only guards the tools it matches, and this one stops edits only. Add Read(./.env) and Edit(./.env) (and the same for./secrets/** ) to permissions.deny in your settings, so Claude can neither read the secrets into its context nor change them with its file tools.

Bash remains the gap: narrow it with Bash deny rules and Claude Code’s Bash sandboxing ( /sandbox ).

In our subagents guide a file-qa agent measures a rendered vertical video with ffprobe. Its prompt says “run only ffprobe”, but a prompt is a request. A hook in the agent’s own frontmatter turns it into a rule, active only while that agent runs:

--- name: file-qa description: " Technical QA of a rendered vertical video file. Use after every render, before posting." tools: Bash model: haiku hooks: PreToolUse: - matcher: " Bash" hooks: - type: command command: ' python "$CLAUDE_PROJECT_DIR/.claude/hooks/only_ffprobe.py"' --- Enter fullscreen mode Exit fullscreen mode.claude/hooks/only_ffprobe.py

import json import shlex import sys data = json. load ( sys. stdin ) command = data. get ( " tool_input ", {}). get ( " command ", "" ) def decide ( decision, reason ): print ( json. dumps ({ " hookSpecificOutput ": { " hookEventName ": " PreToolUse ", " permissionDecision ": decision, " permissionDecisionReason ": reason, }})) sys. exit ( 0 ) if any ( ch in command for ch in ";&|<>`$ \n\r {}*?[]~#() " ): decide ( " deny ", " One plain ffprobe command only: no pipes, redirects, chaining, substitutions, globs or braces. " ) try: args = shlex. split ( command ) except ValueError: decide ( " deny ", " Could not parse the command. " ) if not args or args [ 0 ]!= " ffprobe ": decide ( " deny ", " This agent may run ffprobe only.

" ) if any ( a in ( " -o ", " -report " ) for a in args [ 1:]): decide ( " deny ", " ffprobe may not write files here (-o and -report are blocked). " ) decide ( " allow ", " Plain ffprobe probe. " ) Enter fullscreen mode Exit fullscreen mode Rejecting shell metacharacters first matters: ffprobe x.mp4; rm -rf renders starts with ffprobe too.

So does ffprobe -o, which writes its output to a file, hence the last check. Braces matter too: ffprobe x.mp4 {-o,out.txt} expands to -o out.txt before ffprobe runs, so they are refused with the other shell characters. A deny from a hook holds in every permission mode, even bypassPermissions.

An allow skips the permission prompt but never overrides a deny rule in your settings.

import json import os import subprocess import sys import tempfile from pathlib import Path MAX_TRIES = 3 data = json. load ( sys. stdin ) counter = Path ( tempfile. gettempdir ()) / f " claude-stop- { data. get ( ' session_id ', ' default ' ) }.txt " tries = int ( counter. read_text ()) if counter. exists () else 0 project = os. environ. get ( " CLAUDE_PROJECT_DIR " ) or data. get ( " cwd " ) result = subprocess. run ( " npm test --silent ", shell = True, capture_output = True, text = True, cwd = project, env = { ** os. environ, " CI ": " true " }, # CI=true keeps test runners out of watch mode ) if result. returncode == 0 or tries >= MAX_TRIES: counter. unlink ( missing_ok = True ) # tests pass, or out of tries: let Claude stop sys. exit ( 0 ) counter. write_text ( str ( tries + 1 )) tail = ( result. stdout + result. stderr )[ - 1500:] reason = f " Tests fail (try { tries + 1 } of { MAX_TRIES } ). Fix them before finishing: \n { tail } " print ( json. dumps ({ " decision ": " block ", " reason ": reason })) Enter fullscreen mode Exit fullscreen mode When the suite fails, Claude gets the failing output as its next instruction instead of ending the turn, up to three times in a row; after that Claude may stop, the counter resets and you decide. The official docs suggest checking the stop_hook_active input field to avoid endless loops, but that allows only one retry; a small counter allows a few.

Claude Code also overrides a Stop hook that blocks eight times in a row with no tool call in between. Stop fires whenever Claude finishes a reply, so keep the suite fast or run only the tests for what changed. Register it with a timeout longer than your suite takes (in seconds; Stop needs no matcher):

{ "hooks": { "Stop": [ { "hooks": [{ "type": "command", "command": "python \" $CLAUDE_PROJECT_DIR/.claude/hooks/tests_before_stop.py \" ", "timeout": 300 }] } ] } } Enter fullscreen mode Exit fullscreen mode Example 4: add context after a tool runs (PostToolUse) A PostToolUse hook cannot undo anything, but it can tell Claude something. This one, registered for Edit|Write, warns Claude when it edits a generated file so the next change goes to the spec instead:

{ "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "python \" $CLAUDE_PROJECT_DIR/.claude/hooks/generated_note.py \" " }] } ] } } Enter fullscreen mode Exit fullscreen mode.claude/hooks/generated_note.py

import json import sys data = json. load ( sys. stdin ) path = data. get ( " tool_input ", {}). get ( " file_path ", "" ). replace ( " \\ ", " / " ) if " /src/generated/ " in path: print ( json. dumps ({ " hookSpecificOutput ": { " hookEventName ": " PostToolUse ", " additionalContext ": " This file is generated from the episode spec and is overwritten on the next render. " " Make the change in episodes/<id>/spec.json instead. ", }})) Enter fullscreen mode Exit fullscreen mode To re-inject key rules after the conversation is compacted, a SessionStart hook with the matcher compact can print them; plain stdout from SessionStart goes straight into Claude’s context.

{ "hooks": { "Notification": [ { "hooks": [{ "type": "command", "command": "notify-send 'Claude Code' 'Claude Code needs your attention'" }] } ] } } Enter fullscreen mode Exit fullscreen mode Put it in your user settings so it works in every project. On macOS, use osascript -e 'display notification "Claude Code needs your attention" with title "Claude Code"' as the command; the official hooks guide also has a PowerShell version for Windows.

Hooks complete the set: CLAUDE.md holds the project rules, a skill says how to do a job, subagents check it with a fresh context, and hooks enforce the lines nobody may cross. If you make short vertical videos with Claude Code, the skill below packages the spec-first workflow that Examples 2 and 4 come from.

Animated horror reels skill: Our whole playbook as a Claude Code agent skill: the hook-first story method, the episode spec template, the camera, light and grade system with a palette builder, a render-ready Remotion starter and the QA checks before you post. Make reels like ours. See the skill

Disclosure: this guide is by Homingo, the team behind the skill linked above. Everything in it works without it.

Originaltitel: Claude Code hooks: PreToolUse, PostToolUse and Stop examplesQuelle: dev.to (dev.to). Original öffnen