Claude Code Hooks Tutorial

How to configure Claude Code hooks to run shell commands before or after tool calls. Automate linting, testing, notifications, and logging on every Claude Code action.

💥 50p impulse-buy: Power Prompts PDF (first 10 buyers) 30 battle-tested Claude Code prompts · 8-page PDF · paste into CLAUDE.md and never re-type a prompt again · 50p impulse-buy, no commitment

Claude Code hooks let you attach shell commands to tool lifecycle events — auto-lint on every edit, block writes to protected files, log all commands to audit trail, or ping a webhook when a session completes.

Hook configuration structure

// ~/.claude/settings.json or .claude/settings.json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",           // tool name to match (or "*" for all)
        "hooks": [
          {
            "type": "command",
            "command": "npm run lint -- --fix $CLAUDE_TOOL_INPUT_FILE_PATH"
          }
        ]
      }
    ],
    "Stop": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Claude Code session ended' >> ~/.claude/session.log"
          }
        ]
      }
    ]
  }
}

Auto-lint after every file edit

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "hooks": [
          {
            "type": "command",
            "command": "cd $CLAUDE_PROJECT_ROOT && npx eslint --fix "$CLAUDE_TOOL_INPUT_FILE_PATH" 2>&1 | head -20"
          }
        ]
      },
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "cd $CLAUDE_PROJECT_ROOT && npx prettier --write "$CLAUDE_TOOL_INPUT_FILE_PATH""
          }
        ]
      }
    ]
  }
}

Block writes to protected directories (exit code 2)

// save as .claude/hooks/guard-migrations.sh
#!/bin/bash
# Exit 2 = block the tool call and show stderr to Claude

FILE="$CLAUDE_TOOL_INPUT_FILE_PATH"
if [[ "$FILE" == *"/migrations/"* ]]; then
  echo "BLOCKED: Do not auto-edit migration files. Create a new migration instead." >&2
  exit 2
fi
exit 0
// settings.json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit",
        "hooks": [{"type": "command", "command": "bash .claude/hooks/guard-migrations.sh"}]
      },
      {
        "matcher": "Write",
        "hooks": [{"type": "command", "command": "bash .claude/hooks/guard-migrations.sh"}]
      }
    ]
  }
}

Run tests after code changes

// Run the test file matching the edited source file
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'TEST=$(echo "$CLAUDE_TOOL_INPUT_FILE_PATH" | sed "s/src\//tests\//;s/\.py$/_test.py/"); [ -f "$TEST" ] && python -m pytest "$TEST" -x -q 2>&1 | tail -10 || true'"
          }
        ]
      }
    ]
  }
}

Audit log: record every tool call

// Append timestamp + tool + args to audit log
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) TOOL=$CLAUDE_TOOL_NAME INPUT=$(echo $CLAUDE_TOOL_INPUT | head -c 200)" >> ~/.claude/audit.log"
          }
        ]
      }
    ]
  }
}

Available environment variables in hooks

VariableValueAvailable on
CLAUDE_TOOL_NAMEName of the tool being called (Edit, Bash, Read…)All PreToolUse / PostToolUse
CLAUDE_TOOL_INPUTJSON-encoded tool input (full args)All PreToolUse / PostToolUse
CLAUDE_TOOL_INPUT_FILE_PATHfile_path arg (when tool has one)Edit, Write, Read
CLAUDE_TOOL_OUTPUTTool output (PostToolUse only)PostToolUse
CLAUDE_PROJECT_ROOTAbsolute path to the project directoryAll events
CLAUDE_SESSION_IDUnique ID for this Claude Code sessionAll events

Hook exit codes

Exit codeBehavior
0Success — proceed normally
1Non-fatal warning — proceed but show stderr to user
2 (PreToolUse only)BLOCK — cancel the tool call, show stderr as feedback to Claude

Frequently asked questions

What are Claude Code hooks?
Hooks are shell commands configured in settings.json that run automatically before or after Claude Code tool calls. For example, you can run `npm run lint` after every file edit, or send a Slack notification when Claude Code stops.
Where do I configure Claude Code hooks?
In ~/.claude/settings.json (global) or .claude/settings.json in your project root (project-level). Project settings override global settings.
Which events can I hook into?
Current hook events: PreToolUse (before a tool call), PostToolUse (after a tool call), Stop (when Claude Code finishes a session), and Notification (when Claude sends a desktop notification). More events are added in each release.
Can hooks block Claude Code from proceeding?
Yes. If a PreToolUse hook exits with code 2, Claude Code blocks the tool call and shows the hook's stderr output to the model as feedback. Exit 0 allows the action; exit 1 allows it but shows a warning.
Are hooks secure?
Hooks run with your user permissions — the same risk as any shell script. Review hook scripts before adding them. Claude Code will show you any hook that attempts to run and you can approve or deny execution.

Free tools

Cost Calculator → API Cookbook → Diff Summarizer → Skills Browser →

More examples

Claude API Python QuickstartClaude API Node.js / TypeScript QuickstartClaude API Streaming in PythonClaude API Streaming in Node.js / TypeScriptClaude API Tool Use in PythonClaude API Tool Use in Node.js / TypeScript