Skip to main content
Featured Hooks Automation Tutorial

Claude Code Hooks: Build Automated Dev Workflows

Claude Code Hooks beginner tutorial. Learn Hooks from scratch with 10+ practical examples, common error troubleshooting, and best practices guide.

January 19, 2026 12 min read By Claude World

Want Claude Code to automatically run lint, tests, and security checks? Hooks is the feature you need. This tutorial takes you from zero to mastering Hooks concepts and practical applications.


What are Hooks?

Simply put, Hooks are interceptors for Claude’s actions:

You give command → [Hook intercepts] → Claude executes → [Hook checks] → Done

You can inject your own logic at these points:

TimingHook NamePurpose
Before tool executionPreToolUseBlock dangerous operations, add reminders
After tool executionPostToolUseAuto lint, run tests
When task completesStopEnsure tests pass before ending
When session startsSessionStartLoad environment, display info

5-Minute Quick Start

Step 1: Create Config File

Create .claude/settings.json in your project directory:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'File written!'",
            "onFailure": "ignore"
          }
        ]
      }
    ]
  }
}

Step 2: Test the Hook

Start Claude Code:

claude

Ask Claude to create any file:

You: Create a hello.txt file with content "Hello World"

You’ll see Claude output File written! after creating the file.

Congratulations! Your first Hook is working.


Core Concepts

Hook Structure

Each Hook configuration has three parts:

{
  "hooks": {
    "EventName": [
      {
        "matcher": "Tool to intercept",
        "hooks": [
          {
            "type": "command or prompt",
            "command": "command to execute",
            "onFailure": "failure handling"
          }
        ]
      }
    ]
  }
}

Matchers

MatcherDescription
"Write"Only intercept Write tool
"Edit"Only intercept Edit tool
"Bash"Only intercept Bash tool
"Read"Only intercept Read tool
"*"Intercept all tools
"Write|Edit"Intercept Write or Edit

Hook Types

1. Command Hook (Execute commands)

{
  "type": "command",
  "command": "npm run lint",
  "onFailure": "warn"
}

2. Prompt Hook (Use AI to judge)

{
  "type": "prompt",
  "prompt": "Check if this command is safe. If it contains rm -rf, return 'block'. Otherwise return 'approve'."
}

onFailure Options

OptionBehavior
"block"Block operation on command failure
"warn"Show warning but continue
"ignore"Ignore failure

10 Practical Examples

1. Auto-Format (Lint After Write)

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "npx prettier --write \"$CLAUDE_FILE_PATHS\"",
            "onFailure": "warn"
          }
        ]
      }
    ]
  }
}

2. Auto-Run Tests

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "npm test -- --passWithNoTests",
            "onFailure": "warn"
          }
        ]
      }
    ]
  }
}

3. Block Dangerous Commands

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Check if command contains dangerous operations (rm -rf, DROP TABLE, git push --force). If yes, return 'block'. Otherwise return 'approve'."
          }
        ]
      }
    ]
  }
}

4. Prevent Reading Secret Files

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Read",
        "hooks": [
          {
            "type": "prompt",
            "prompt": "If file path contains .env, secret, credential, password, return 'block'. Otherwise return 'approve'."
          }
        ]
      }
    ]
  }
}

5. Check for API Key Leaks

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Scan content for API keys (sk_, api_key, token). If found, return 'block' with explanation. Otherwise return 'approve'."
          }
        ]
      }
    ]
  }
}

6. Force Tests to Pass Before Completion

{
  "hooks": {
    "Stop": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "npm test",
            "onFailure": "block"
          }
        ]
      }
    ]
  }
}

7. Display Session Start Message

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Claude Code started - Project: '$(basename $(pwd))",
            "onFailure": "ignore"
          }
        ]
      }
    ]
  }
}

8. TypeScript Type Check

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "npx tsc --noEmit",
            "onFailure": "warn"
          }
        ]
      }
    ]
  }
}

9. Git Commit Pre-Check

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "prompt",
            "prompt": "If command is git commit or git push, confirm tests have passed. If uncertain, return 'ask' to query user. Otherwise return 'approve'."
          }
        ]
      }
    ]
  }
}

10. Sensitive Directory Protection

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "prompt",
            "prompt": "If file is in auth/, payment/, admin/ directory, return 'ask' for confirmation. Otherwise return 'approve'."
          }
        ]
      }
    ]
  }
}

Common Error Troubleshooting

Error 1: Hook Not Triggering

Symptom: Configured Hook but it doesn’t execute

Checklist:

  1. Is JSON format correct? Validate with jq . .claude/settings.json
  2. Is Matcher spelled correctly? (Write not write)
  3. Is file location correct? (.claude/settings.json)
  4. Did you restart Claude?

Solution:

# Validate JSON format
cat .claude/settings.json | jq .

# Restart Claude
claude

Error 2: Command Hook Failing

Symptom: Hook command failed with exit code X

Common Causes:

  • Command path not found
  • Insufficient permissions
  • Command itself has errors

Solution:

# Test command manually first
npm test

# Confirm command is in PATH
which npm

Error 3: Prompt Hook Not Working as Expected

Symptom: Hook makes wrong judgment or doesn’t respond

Improvement Method:

Bad prompt:

{
  "prompt": "Check if this is safe"
}

Good prompt:

{
  "prompt": "Check if command contains 'rm -rf'. If yes, return 'block'. Otherwise return 'approve'. Return only one of these two values."
}

Error 4: Environment Variable Issues

Symptom: $CLAUDE_FILE_PATHS is empty

Solution: Only certain Hook events have specific variables

VariableAvailable Hooks
$CLAUDE_FILE_PATHSPostToolUse (after Write/Edit)
$CLAUDE_PROJECT_DIRAll Hooks

Error 5: Infinite Loop

Symptom: Hook triggers Hook, gets stuck

Cause: Command executed by Hook triggers another Hook

Solution: Use more precise matcher or avoid triggering other tools in command


Best Practices

1. Start Simple

Don’t configure too many Hooks at once. Start with one:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'File written!'",
            "onFailure": "ignore"
          }
        ]
      }
    ]
  }
}

2. Use onFailure Appropriately

ScenarioRecommended Setting
Tests must pass"onFailure": "block"
Lint can have warnings"onFailure": "warn"
Non-critical checks"onFailure": "ignore"

3. Layered Configuration

PreToolUse → Prevent dangerous operations (Security)
PostToolUse → Automated checks (Quality)
Stop → Confirm completion standards (Delivery)

4. Team Sharing

Add .claude/settings.json to Git so team shares the same Hooks:

git add .claude/settings.json
git commit -m "Add Claude Code hooks for team standards"

Complete Configuration Example

Here’s a starting configuration suitable for most projects:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Check if command contains dangerous operations (rm -rf, DROP, --force). If yes, return 'block'. Otherwise return 'approve'."
          }
        ]
      },
      {
        "matcher": "Read",
        "hooks": [
          {
            "type": "prompt",
            "prompt": "If path contains .env or secret, return 'block'. Otherwise return 'approve'."
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "npm run lint -- --quiet 2>/dev/null || true",
            "onFailure": "ignore"
          }
        ]
      }
    ],
    "Stop": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "npm test -- --passWithNoTests 2>/dev/null",
            "onFailure": "warn"
          }
        ]
      }
    ],
    "SessionStart": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Project: '$(basename $(pwd))",
            "onFailure": "ignore"
          }
        ]
      }
    ]
  }
}

Next Steps

After learning Hooks basics, recommended reading:

  1. Hooks Complete Guide - Advanced configuration and more examples
  2. Hooks Development Manual - Script development details
  3. TDD Workflow - Implement TDD with Hooks
  4. Security Best Practices - Security-related Hook configurations

Summary

You want to…Use this Hook
Block dangerous operationsPreToolUse + prompt
Auto lint/testPostToolUse + command
Ensure tests pass before completionStop + command
Execute at session startSessionStart + command

Remember: Hooks turn “suggestions” into “enforcement”. Configure once, effective forever.


Last updated: 2026-01-19