Web TippsUse custom web fonts in Google Sheets charts(08.09.2026 um 17:05 Uhr)
Web TippsIntroducing the new 1Password App for Google Chat(08.09.2026 um 18:02 Uhr)
Web TippsUse custom web fonts in Google Sheets charts(08.09.2026 um 17:05 Uhr)
Web TippsIntroducing the new 1Password App for Google Chat(08.09.2026 um 18:02 Uhr)

🔧 Programmierung 🕛 vor 1 Monat 6 Min Lesezeit
0

Claude Code hooks explained: config structure, matchers, and a copy-paste PreToolUse guard

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

If you have only ever used Claude Code's permission prompts, hooks are the next layer down: deterministic shell commands (or HTTP endpoints, MCP tools, prompts, or subagents) that fire at fixed points in a session and can block, allow, or reshape what happens next. The feature is powerful, but the configuration has a few sharp edges that trip people up — especially the JSON your hook prints back, which changed and is easy to get wrong from memory.



This post walks through the current config structure, the two ways a hook reports a decision, and a copy-paste PreToolUse guard that blocks recursive rm.






The three-level config structure



Hooks live in a settings file (.claude/settings.json for a project, ~/.claude/settings.json for your user). The shape is three levels of nesting, and naming them makes the rest of the docs click:




CODE
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh",
"if": "Bash(rm *)"
}
]
}
]
}
}








  1. Hook event (PreToolUse) — the lifecycle point. Events fall into three cadences: once per session (SessionStart, SessionEnd), once per turn (UserPromptSubmit, Stop), and on every tool call (PreToolUse, PostToolUse).


  2. Matcher group ("matcher": "Bash") — a filter for when the event fires. For tool events the matcher tests against tool_name.


  3. Hook handler (the inner hooks array) — the thing that actually runs. There are five types: command, http, mcp_tool, prompt, and agent.



The if field narrows further using permission-rule syntax — Bash(rm *) here means "only spawn this handler when the Bash subcommand matches rm *," so the script never even launches for npm test. It holds exactly one rule; there is no && or ||, so multiple conditions mean multiple handlers.






Matchers are unanchored regexes (this bites people)



A matcher that contains regex characters is tested with JavaScript's RegExp.prototype.test, which succeeds on a match anywhere in the value. So:





  • Edit also matches NotebookEdit. If you mean only the Edit tool, write ^Edit$.

  • MCP tools appear as mcp__<server>__<tool>. A bare mcp__memory is treated as an exact string and matches nothing — you need the .* suffix: mcp__memory__.* for every tool from the memory server.



If you want a handler to run on every occurrence of an event, omit the matcher or use "*".






Two ways to report a decision — pick one



This is where most confusion lives. A command hook communicates results through exit codes and stdout, and you choose one of two modes:



Exit codes alone. Exit 0 means success and no decision (staying silent does not approve a call — it just lets the normal permission flow continue). Exit 2 is a blocking error: Claude Code ignores stdout and feeds your stderr back to Claude as the reason. For PreToolUse, exit 2 blocks the tool call.



Exit 0 plus JSON on stdout. For finer control, exit 0 and print a JSON object. Claude Code only parses JSON on exit 0 — if you exit 2, any JSON is ignored. You must choose one approach per hook, not both.






The gotcha: PreToolUse no longer uses decision: "block"



If you learned hooks a while ago, you probably remember a top-level {"decision": "block"}. That is deprecated for PreToolUse. This event now returns its decision inside a hookSpecificOutput object with a required hookEventName, and the field is permissionDecision with four possible values: allow, deny, ask, or defer. (The old "approve"/"block" map to "allow"/"deny".) Other events like PostToolUse and Stop still use the top-level decision/reason fields — so the format genuinely differs per event, and copying a PostToolUse snippet into PreToolUse won't work.



Here is the correct current shape for a PreToolUse deny:




CODE
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Recursive rm is blocked. Delete specific files instead."
}
}









Copy-paste: block recursive rm



Save this as .claude/hooks/block-rm.sh and chmod +x it. It reads the event JSON on stdin, pulls the Bash command with jq, and denies anything with a recursive rm:




CODE
#!/usr/bin/env bash
# PreToolUse hook: deny recursive rm, stay silent otherwise.
# Requires jq on PATH.
input=$(cat)
command=$(printf '%s' "$input" | jq -r '.tool_input.command // ""')

if printf '%s' "$command" | grep -qiE '(^|[[:space:]])rm[[:space:]]+(-[a-z]*r|--recursive)'; then
cat <<'JSON'
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Recursive rm is blocked by a project hook. Delete specific files instead."
}
}
JSON
exit 0
fi

# No decision: exit 0 with no output lets the normal permission flow decide.
exit 0






With the settings above, the if: "Bash(rm *)" filter means the script only spawns for rm commands (saving the process launch on unrelated calls), and the matcher keeps it scoped to Bash. When Claude tries rm -rf /tmp/build, the hook prints the deny JSON, Claude Code blocks the call, and Claude sees the reason.



A note on paths: ${CLAUDE_PROJECT_DIR} is substituted by Claude Code to your project root, so the hook resolves no matter what the working directory is at call time. If your project path contains spaces, switch the handler to exec form (set args) so each element is passed as one argument with no shell quoting.






Where to go from here



Hooks are the deterministic guardrail layer. Two neighbors worth knowing:





  • The if filter is best-effort. For a hard allow/deny that must never fail open, use the permission system rather than a hook — the docs are explicit that a malformed Bash command makes the if filter fail open and run your hook anyway.


  • Skills and subagents can define hooks in frontmatter, scoped to that component's lifetime. If you are writing skills, the trigger mechanics matter as much as the hook wiring — I wrote up the .



Type /hooks in Claude Code to open a read-only browser of everything you have configured, including which settings file each hook came from — a fast way to confirm your matcher is actually matching.






I'm Rulestack — I build and maintain drop-in rule and hook packs for Claude Code, Cursor, and Codex. There's a — follow along if this was useful.

Vollständiger Original-Bericht
Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
3 Quellen
Use custom web fonts in Google Sheets charts
2 Quellen
Introducing the new 1Password App for Google Chat
1 Quelle
Context-aware access controls are available for Gemini Enterprise in the Admin console
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Claude Code hooks explained: config structure, matchers, and a copy-paste PreToolUse guard

Thematisch verwandte Begriffe: Claude, Code, hooks, explained · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...