DaleSchool

Automate Workflows with Hooks

Intermediate20min

Learning Objectives

  • Understand the main Hook event types
  • Block dangerous operations with a PreToolUse Hook
  • Set up automatic post-processing with a PostToolUse Hook

Working Code

Let's add a Hook to .claude/settings.json. This Hook blocks file modifications on the main branch:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "[ \"$(git branch --show-current)\" != \"main\" ] || { echo '{\"block\":true,\"message\":\"Cannot modify files on the main branch. Create a feature branch first.\"}' >&2; exit 2; }",
            "timeout": 5
          }
        ]
      }
    ]
  }
}

Now ask Claude to modify a file on the main branch:

> Add a line to README.md

The Hook triggers and blocks the modification. Switch to a different branch and it works normally:

> Create a branch called feature/test with git checkout -b, then modify README.md

Try It Yourself

Now let's use a PostToolUse Hook to auto-lint after file saves:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "npx eslint --fix $CLAUDE_FILE_PATHS 2>/dev/null || true",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

Every time Claude modifies a file, ESLint automatically runs and fixes the style.

"Why?" — Enforcing Rules with Code

Even if you write "don't work on the main branch" in CLAUDE.md, Claude might ignore it. Hooks enforce rules with code. It's not a request — it's a block.

Key Hook Events

EventWhenPurpose
PreToolUseBefore tool executionBlock dangerous operations, conditional approval
PostToolUseAfter tool executionAuto-formatting, linting, logging
NotificationWhen a notification firesCustom notifications (Slack, desktop, etc.)
StopWhen response completesResult validation, cleanup
UserPromptSubmitWhen user submits inputInput preprocessing
SubagentStopWhen sub-agent completesSub-agent result post-processing
PreCompactBefore context compressionPreserve data before compression
SessionStartWhen session startsEnvironment validation, dependency checks
SessionEndWhen session endsCleanup, logging

The most commonly used are PreToolUse, PostToolUse, and Notification.

Hook Configuration Structure

{
  "hooks": {
    "EventName": [
      {
        "matcher": "tool name pattern (regex)",
        "hooks": [
          {
            "type": "command",
            "command": "shell command to execute",
            "timeout": 5
          }
        ]
      }
    ]
  }
}

Matcher Patterns

The matcher filters which tools the Hook applies to:

PatternMatches
Edit|WriteFile edit/create tools
BashShell command execution
ReadFile reading
.*All tools (use with caution)

Exit Codes Determine Behavior

The Hook's exit code determines what happens:

Exit CodeBehavior
0Success — continue the operation
2Block — prevent tool execution
OtherTreated as error

Returning exit 2 from PreToolUse blocks the operation:

exit 2

Send a message via stderr to tell Claude why it was blocked:

echo "Cannot modify files on the main branch." >&2
exit 2

Deep Dive

What environment variables are available in Hooks?

You can use these environment variables inside Hook commands:

  • $CLAUDE_FILE_PATHS — file paths related to the current tool call (space-separated)
  • $CLAUDE_PROJECT_DIR — absolute path to the project root directory

For more detailed information, parse the JSON passed via stdin with jq:

jq -r '.tool_input.command' >> ~/.claude/bash-log.txt
  1. Add a main branch protection Hook to .claude/settings.json and test it.
  2. Create a PostToolUse Hook that auto-runs prettier --write after file modifications.
  3. Create a SessionStart Hook that prints "Current branch: {branch name}" when a session starts.

Which Hook event runs before Claude Code modifies a file?

Further Reading