Skip to content

Blogclaude-code

Claude Code hooks: automate the steps you repeat

Claude Code hooks run shell commands at set lifecycle points. Set up three first: a desktop notification, Prettier after edits, and protected files.

3hship5 min read

Share

Contents

Claude Code hooks are user-defined shell commands that Claude Code runs at specific points in its lifecycle, so certain actions always happen instead of depending on the model to choose to run them. Three are worth setting up first: a desktop notification, Prettier after every edit, and a block on protected files.

What we used: Claude Code with a user settings file and a project settings file, the macOS notification command from the docs, jq for reading JSON, and Prettier run through npx. The configuration blocks below are the ones in the Claude Code hooks guide, which also has the Linux and Windows notification commands.

What hooks are and when they run

A hook is a shell command attached to a lifecycle event, optionally filtered by a matcher. Because the command runs every time the event fires, hooks give you deterministic control, which a line in a prompt cannot.

Why not just tell Claude what to do? Because an instruction is something the model may or may not follow. For decisions that need judgment, prompt-based hooks and agent-based hooks use a Claude model to evaluate conditions. The rest of this post stays with plain shell commands.

Event Runs when Use it for
Notification Claude is waiting for input or permission Desktop alerts
PreToolUse Before a tool call, filtered by a matcher such as Edit|Write Blocking edits
PostToolUse After file-editing tools when the matcher is Edit|Write Formatting
SessionStart A session starts; the compact matcher covers re-injecting context after compaction Reminders
ConfigChange An external process or editor modifies a configuration file Audit logs
CwdChanged Claude changes directory Reloading environment variables

The docs point to a separate reference for the full list of hook events.

1. Add a notification hook

This hook alerts you when Claude is waiting, so you can leave the terminal.

  1. Open ~/.claude/settings.json, creating it if it does not exist, and add a Notification hook. This is the macOS version:
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
}
]
}
]
}
}
  1. If the file already has a hooks key, add Notification as a sibling of the existing event keys rather than replacing the whole object.
  2. Type /hooks at the Claude Code prompt. The hook should appear under Notification.
  3. Press Shift+Tab until the status bar shows manual mode, ask Claude for something that needs permission, then switch away from the terminal. A desktop notification should arrive.

The empty matcher fires on every notification type. To narrow it, use a value such as permission_prompt, which fires when Claude needs you to approve a tool use and the prompt has waited about six seconds, or idle_prompt, which fires when Claude finished responding about 60 seconds ago and you haven’t typed since.

If you hit usage limits often, the same event family has matchers for when Claude Code resumes after a pause. The usage limits post covers what spends your quota.

2. Format files after every edit

Prettier runs on each file Claude edits, so formatting stays consistent without you asking.

  1. Add this to .claude/settings.json in your project root. The Edit|Write matcher means it runs only after file-editing tools:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
  1. Install jq if it is missing. The docs give brew install jq on macOS and apt-get install jq on Debian and Ubuntu.
  2. Ask Claude to add a line with single-quoted strings to a JavaScript file, then open the file. With Prettier’s default settings, the hook rewrites them to double quotes.

A successful hook is silent, since Claude Code shows nothing in the conversation. Check the file, not the chat. If you need a file reformatted however it changes, including when a Bash command rewrites it, the docs recommend a FileChanged hook instead.

3. Block edits to protected files

This hook stops Claude from touching .env, package-lock.json or anything in .git/. Claude receives feedback explaining why the edit was blocked, so it can adjust its approach.

  1. Save this script to .claude/hooks/protect-files.sh:
#!/bin/bash
# protect-files.sh (from the Claude Code docs; run inside your project)
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
  1. Make it executable on macOS and Linux, because hook scripts must be executable for Claude Code to run them:
Terminal window
chmod +x .claude/hooks/protect-files.sh
  1. Register it as a PreToolUse hook in .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
}
]
}
]
}
}
  1. Ask Claude to add a comment to your .env file. The edit is blocked before it runs, and the script’s Blocked: message goes to Claude as feedback.

The rule that makes this work is the exit code. The script exits with code 2 to block the edit; exiting 0 lets it through.

Terminal window
exit 2 # block the edit
exit 0 # let it through

4. Repeat it for the next hook

Every hook above follows the same five moves, so a new one takes a few minutes.

  1. Pick the event that matches the moment: before a tool call, after one, at session start, or when Claude waits.
  2. Set a matcher so the hook fires only on the tools or notification types you care about.
  3. Write the command, or point to a script that reads the JSON from standard input.
  4. Add it to the right settings file: ~/.claude/settings.json for every project, .claude/settings.json for one project.
  5. Open /hooks to confirm it is listed, then trigger it once on purpose.

Two good next candidates are already in the docs. A SessionStart hook with a compact matcher can re-inject critical context after every compaction, because compaction can lose important details. A ConfigChange hook can append each settings or skills change to an audit log. Hooks cover actions that must always happen; for extra instructions, skills are the other tool, and the shared skills repo post shows how to keep one set across Claude Code and Codex.

Verdict

Start with the notification, the Prettier hook and the protected-files block: each is a single settings entry, and each is confirmed by one test action. Use /hooks to check that a hook is registered, and expect the formatting hook to stay silent when it works. Move to compaction reminders or a config audit log once those three behave.

Try it today: read the Claude Code hooks guide

Sources:Claude Code docs: Automate actions with hooks.

Responses

Sign in to write a response

Keep reading