ASK KNOX
beta
LESSON 58

Hooks — Automating Claude Code Workflows

Hooks fire before and after every tool call. Use them to auto-lint, auto-test, send notifications, block dangerous commands, and build workflows that run themselves. Here is the complete playbook.

10 min read·Advanced Claude Code

Every tool call Claude makes — every Edit, every Bash, every Write — passes through two event gates: one before it executes and one after. Hooks are your scripts at those gates. You can run any shell command, inspect context, block execution, enforce quality standards, send notifications, or fire off side effects. No extra tool required. The plumbing is already there.

This is how you build a Claude Code environment that enforces its own quality standards without you having to ask.

Hooks — Automating Claude Code Workflows

The Event Model

Four hook events are available:

PreToolUse — fires before a tool executes. If your script exits with code 2, the tool call is blocked. If it exits with 0, execution continues. Use this for validation: is Claude about to run a dangerous bash command? Is it about to edit a protected file?

PostToolUse — fires after a tool executes successfully. The tool result is available in the environment. Use this for side effects: run the linter after every file edit, run the test suite after a file that matches *.test.* is written, send a webhook after a deployment completes.

Notification — fires when Claude sends a user-facing notification (asking for permission, reporting completion, etc.). Use this to trigger external alerts — a Discord message, a macOS notification, a Slack ping.

Stop — fires when the session ends. Use this to clean up temp files, commit a session log, send a summary.

Configuration

Hooks live in .claude/settings.json at the project root, or in ~/.claude/settings.json for global hooks.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "npm run lint --silent 2>&1 | head -20"
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/validate-bash.sh"
          }
        ]
      }
    ],
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/session-summary.sh"
          }
        ]
      }
    ]
  }
}

The matcher field is a regex matched against the tool name. Edit|Write matches any Edit or Write call. Bash matches only bash. An empty string "" matches everything.

Practical Hook Patterns

Auto-Lint on Every Edit

The most common hook. Catch lint errors immediately, before they accumulate:

#!/bin/bash
# ~/.claude/hooks/auto-lint.sh
# Runs after every Edit or Write
cd "$CLAUDE_PROJECT_DIR" 2>/dev/null || exit 0
npx eslint --fix --quiet "$CLAUDE_TOOL_INPUT_PATH" 2>&1 | head -10
exit 0  # Never block — just report

Auto-Test Trigger

Run the test suite whenever a test file is written:

#!/bin/bash
# Triggered by PostToolUse matcher: "Edit|Write"
if [[ "$CLAUDE_TOOL_INPUT_PATH" == *".test."* ]]; then
  cd "$CLAUDE_PROJECT_DIR"
  npm test -- --run "$CLAUDE_TOOL_INPUT_PATH" 2>&1 | tail -20
fi
exit 0

Blocking Dangerous Bash Commands

#!/bin/bash
# PreToolUse matcher: "Bash"
# Block rm -rf on anything outside /tmp
CMD="$CLAUDE_TOOL_INPUT_COMMAND"
if echo "$CMD" | grep -qE 'rm -rf [^/tmp]'; then
  echo "BLOCKED: rm -rf outside /tmp requires explicit confirmation" >&2
  exit 2  # Exit 2 blocks the tool call
fi
exit 0

Discord Notification on Session Stop

#!/bin/bash
# Stop hook — sends session summary to Discord
WEBHOOK_URL="$DISCORD_WEBHOOK_URL"
MESSAGE="Claude Code session ended. Project: $(basename $PWD)"
curl -s -X POST "$WEBHOOK_URL" \
  -H "Content-Type: application/json" \
  -d "{\"content\": \"$MESSAGE\"}" > /dev/null
exit 0

Hooks are the enforcement layer — the people equivalent for automated systems. They make the rules real.

Environment Variables Available to Hooks

Claude Code injects context into the hook environment:

VariableContent
CLAUDE_TOOL_NAMEThe tool being invoked (Edit, Bash, Write, etc.)
CLAUDE_TOOL_INPUT_PATHFile path for file tools
CLAUDE_TOOL_INPUT_COMMANDCommand string for Bash
CLAUDE_PROJECT_DIRAbsolute path to the project root
CLAUDE_SESSION_IDUnique session identifier

Use these to write hooks that respond to the specific context — not every edit, but edits to specific paths; not every bash call, but ones that match dangerous patterns.

Security Considerations

Hooks run as shell commands with your full user permissions. A few rules that are not optional:

Never put secrets in hook scripts that are committed. Your Discord webhook URL, database credentials, API keys — these go in environment variables that the hook reads from your shell, not hardcoded in the script source.

Validate hook script inputs. If CLAUDE_TOOL_INPUT_COMMAND is used in a hook, treat it as untrusted input. Do not eval it or pass it to shell expansion without sanitizing.

Test hooks with exit 0 first. Write the hook to do nothing but print what it would do. Confirm the logic is correct before connecting it to real side effects.

Writing Hook Scripts

A few production-grade conventions:

#!/opt/homebrew/bin/bash
# Always use the explicit Homebrew bash path on macOS for launchd compatibility
set -euo pipefail   # Fail fast on any error

# Log to a file for debugging — never to stdout unless you want it in Claude's context
LOG="$HOME/.claude/hooks/hook-$(date +%Y%m%d).log"
echo "[$(date -u +%H:%M:%S)] Tool: $CLAUDE_TOOL_NAME Path: ${CLAUDE_TOOL_INPUT_PATH:-none}" >> "$LOG"

# Do your work here
# ...

exit 0

Store hook scripts in ~/.claude/hooks/ and make them executable with chmod +x. Reference them by absolute path in your settings configuration.

Lesson 58 Drill

Implement the auto-lint hook. Create .claude/settings.json with the PostToolUse config pointing to a script that runs your project's linter after every Edit or Write. Run a Claude session, make a change, and observe the hook output appearing in Claude's context. Then add one more hook — a Stop hook that writes a one-line session log entry to ~/.claude/session-log.txt.

Two hooks running and you will feel the difference within a day of use.

Bottom Line

Hooks turn Claude Code from a smart assistant into an enforcing system. PreToolUse validates and blocks. PostToolUse triggers quality side effects. Notification routes alerts. Stop handles cleanup. Exit code 2 blocks. Exit code 0 continues. Keep hook scripts tight, log to files not stdout, never commit secrets, and test with passive scripts before enabling blocking logic. The environment variables give you enough context to build any enforcement rule you need.