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 0Hook 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
/hooksto confirm it is listed under the right event. Matchers are case-sensitive.PreToolUsefires before a tool runs andPostToolUsefires 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 runecho $?[1]. - If
/hooksshows nothing, check that the JSON is valid. Trailing commas and comments are not allowed. Project hooks go in.claude/settings.jsonand global hooks in~/.claude/settings.json[1]. - When building JSON output, use a JSON encoder such as
jqrather 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
- 1Automate 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