TellWorldAI World Archive
Ask中文

Home›Practice›Practice

Practice

Claude Code hooks in practice

Hooks are shell commands that Claude Code runs at fixed points in its lifecycle. This guide walks through two working examples, auto-formatting after edits and blocking edits to protected files, and explains how to debug a hook that does not fire.

Who this is for and what you will be able to do

This guide is for people who want something to happen every time, or never happen. By the end you should be able to add a desktop notification in settings.json, auto-format edited files, block edits to sensitive files, and check why a hook is not firing [1].

Hooks versus CLAUDE.md

A hook is a shell command that you configure and Claude Code runs at specific points in its lifecycle. The guide describes this as deterministic control: an action always happens rather than relying on the model to choose to run it. Hooks are the right tool for enforcing project rules and automating repetitive steps. Where a judgment is needed, prompt-based or agent-based hooks can use a Claude model to evaluate the condition [1].

First hook: a notification when Claude needs you

The simplest example adds a Notification hook to ~/.claude/settings.json. On macOS the command is osascript -e 'display notification "Claude Code needs your attention" with title "Claude Code"', which shows a desktop notification when Claude waits for input [1]. If the settings file already has a hooks key, add Notification as a sibling of the existing event keys rather than replacing the whole object [1].

To confirm the setup, type /hooks at the Claude Code prompt. The hooks browser lists the new hook under its event [1].

Auto-format edited files with Prettier

Add a PostToolUse hook with the matcher Edit|Write to .claude/settings.json. The hook fires only after the file-editing tools run. Its command is:

f=$(jq -r '.tool_input.file_path') && npx prettier --write --ignore-unknown "$f"

When the hook succeeds, nothing appears in the conversation, so check that the edited file was reformatted [1]. To reformat a file however it changed, including when a Bash command rewrote it, the guide suggests a FileChanged hook instead [1]. The Bash examples use jq, which you can install with brew install jq on macOS [1].

Block edits to protected files

This example uses a script. Save the following as .claude/hooks/protect-files.sh:

#!/bin/bash
# protect-files.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Normalize Windows backslash separators so the patterns below match
FILE_PATH="${FILE_PATH//\\//}"
PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")
for pattern in "${PROTECTED_PATTERNS[@]}"; do
  if [[ "$FILE_PATH" == *"$pattern"* ]]; then
    echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
    exit 2
  fi
done
exit 0

Hook scripts must be executable, so run chmod +x .claude/hooks/protect-files.sh on macOS and Linux [1]. Then register a PreToolUse hook with the matcher Edit|Write and the command "$CLAUDE_PROJECT_DIR"/.claude/hooks/protect-files.sh [1]. To test it, ask Claude to add a comment to .env. Claude Code blocks the edit before it runs and passes the script's Blocked: message back to Claude as feedback [1].

Why a hook does not fire

  • If a hook is configured but never runs, run /hooks to confirm it is listed under the right event. Matchers are case-sensitive. PreToolUse fires before a tool runs and PostToolUse fires after it [1].
  • For a hook error, pipe sample JSON into the script and check the exit code, for example echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh, then run echo $? [1].
  • If /hooks shows nothing, check that the JSON is valid. Trailing commas and comments are not allowed. Project hooks go in .claude/settings.json and global hooks in ~/.claude/settings.json [1].
  • When building JSON output, use a JSON encoder such as jq rather than string concatenation, so quotes and backslashes in values are escaped [1].

Pitfalls

PostToolUse hooks cannot undo an action, because the tool has already run. Use PreToolUse to stop an action before it happens [1].

A Stop hook fires whenever Claude finishes responding, not only when a task is complete, and it does not fire on user interrupts. If it blocks eight times in a row with no tool call in between, Claude Code overrides it. The hook should read the stop_hook_active field from its input and exit early if it has already triggered a continuation [1].

Hooks cannot loosen permission rules. A hook that returns permissionDecision: "deny" blocks the tool even in bypassPermissions mode, while a hook returning allow does not override deny rules in your settings [1].

When several hooks match the same event, every command runs to completion before the results are merged. For PreToolUse permission decisions the most restrictive answer applies, in the order deny, defer, ask, allow [1].

Command hooks communicate only through stdout, stderr and exit codes, and they cannot trigger slash commands or tool calls. The default timeout is 10 minutes, and a per-hook timeout field in seconds overrides it [1].

Next steps

Start with a simple hook such as a notification or formatting, confirm it behaves as expected, and then add blocking rules. The full list of events and JSON formats is in the official hooks reference [1].

Sources

  1. 1
    Automate actions with hooksAnthropic Claude Code docs

Cite this page: Claude Code hooks in practice. TellWorld, https://www.tellworld.net/practice/practice-claude-code-hooks. Updated 2026-11-14

Related news

View all