DEV Community

brian austin
brian austin

Posted on

Claude Code hooks: auto-format, auto-test, and self-heal on every save

Claude Code hooks: auto-format, auto-test, and self-heal on every save

Claude Code hooks let you run shell commands automatically before or after Claude uses a tool. This means you can build a fully automated loop: Claude edits a file → your linter runs → tests execute → if something breaks, Claude fixes it.

Here's exactly how to set it up.

What hooks actually do

Hooks fire at specific events in Claude Code's tool execution lifecycle:

  • PreToolUse — runs before Claude uses a tool (read file, write file, bash, etc.)
  • PostToolUse — runs after Claude uses a tool
  • Stop — runs when Claude finishes a turn
  • Notification — runs when Claude sends you a notification

You configure them in ~/.claude/settings.json (global) or .claude/settings.json (project).

The auto-format hook

This hook runs Prettier automatically whenever Claude writes a file:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "prettier --write $CLAUDE_TOOL_INPUT_FILE_PATH 2>/dev/null || true"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

$CLAUDE_TOOL_INPUT_FILE_PATH is the environment variable Claude Code sets to the file path being written. Every time Claude edits a file, Prettier runs on it automatically. No manual formatting.

The auto-test hook

Run your test suite after every file write:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "npm test --silent 2>&1 | tail -5"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Claude sees the test output. If tests fail, it knows immediately and can fix the file before moving to the next task.

Combining format + test + auto-fix

Here's the full self-healing loop:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "prettier --write $CLAUDE_TOOL_INPUT_FILE_PATH 2>/dev/null || true && npm test --silent 2>&1 | tail -10"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

The flow:

  1. Claude writes a file
  2. Prettier formats it automatically
  3. Tests run
  4. If tests fail, Claude sees the output in its context
  5. Claude fixes the failure
  6. Repeat until tests pass

This turns Claude Code into a proper TDD loop without you manually running anything.

The .env blocker hook

Prevent Claude from reading your secrets file:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Read",
        "hooks": [
          {
            "type": "command",
            "command": "if [[ $CLAUDE_TOOL_INPUT_FILE_PATH == *'.env'* ]]; then echo 'BLOCKED: .env files are not accessible to Claude'; exit 1; fi"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Exit code 1 from a PreToolUse hook cancels the tool call. Claude gets the error message in its context and won't try again.

The ESLint hook (with auto-fix)

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "eslint --fix $CLAUDE_TOOL_INPUT_FILE_PATH 2>&1 | head -20"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

--fix auto-corrects fixable lint errors. The remaining unfixable ones get printed to stdout, which Claude sees and can address manually.

The Stop hook — run after every Claude turn

Stop hooks fire when Claude finishes responding. Useful for running a full test suite at the end of a session:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "npm test 2>&1 | tail -20"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

This gives you a final health check at the end of every Claude Code session.

Combining everything in one settings file

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Read",
        "hooks": [
          {
            "type": "command",
            "command": "if [[ $CLAUDE_TOOL_INPUT_FILE_PATH == *'.env'* ]]; then echo 'BLOCKED: cannot read .env'; exit 1; fi"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "prettier --write $CLAUDE_TOOL_INPUT_FILE_PATH 2>/dev/null || true && eslint --fix $CLAUDE_TOOL_INPUT_FILE_PATH 2>&1 | head -5 || true && npm test --silent 2>&1 | tail -10"
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Session complete. Running final test suite...' && npm test 2>&1 | tail -20"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Why this matters for rate limits

The self-healing loop means Claude finishes tasks faster with fewer back-and-forth turns. Fewer turns = fewer tokens = less chance of hitting rate limits mid-session.

If you're hitting Claude Code rate limits anyway, the pattern is to route through a proxy that removes the limit entirely:

export ANTHROPIC_BASE_URL=https://simplylouie.com
Enter fullscreen mode Exit fullscreen mode

Set that in your shell profile and claude uses it automatically. Rate limits gone. ✌️$2/month at simplylouie.com.

The matcher field

The matcher field accepts any Claude Code tool name:

  • Write — file writes
  • Read — file reads
  • Bash — bash commands
  • Edit — file edits (alternative to Write in some versions)
  • Leave it empty to match all tools

Summary

  • PostToolUse Write = auto-format + auto-test after every file edit
  • PreToolUse Read = block sensitive files before Claude reads them
  • Stop = full suite at end of session
  • $CLAUDE_TOOL_INPUT_FILE_PATH = the file being written (use in your hook commands)
  • Exit code 1 in PreToolUse = cancels the tool call

Hooks turn Claude Code from a chat interface into a proper CI loop running in your terminal. Once configured, you stop running lint and tests manually entirely.

Top comments (0)