ai newsMo., 2. Nov.
EilmeldungLinux Kernel 7.4-rc1 mit fast 20 000 Commits und AI-ggetriebenen Fixes

Claude Code hooks: PreToolUse, PostToolUse and Stop examples

Originally published on Homingo Learn . CLAUDE.md asks; a hook makes sure. How hooks work, the JSON they read and return, and five examples to adapt today. What are Claude Code hooks? A hook is a command that Claude Code runs at a fixed point in its lifecycle: before a tool call ( PreToolUse ), aft

dev.toMontag 14:49 · 02.11.9 Min. Lesezeit
Bild: dev.to

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.

Quelle: dev.to (dev.to). Original öffnen