DEV Community

brian austin
brian austin

Posted on

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

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

Claude Code hooks let you run shell commands automatically at key moments in your session — before Claude reads a file, after it writes one, or when a tool call completes. This is how you build a self-healing loop where Claude formats, tests, and fixes code without you having to ask.

What are hooks?

Hooks are defined in your .claude/settings.json file. They fire at lifecycle events during Claude's execution.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "npm run lint --silent"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Every time Claude writes or edits a file, this runs npm run lint automatically. Claude sees the output and fixes any lint errors before moving on.

The four hook events

Event When it fires
PreToolUse Before Claude calls a tool
PostToolUse After Claude calls a tool
Notification When Claude sends a notification
Stop When Claude finishes responding

Hook 1: Auto-format on write

This runs Prettier after every file write:

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

$CLAUDE_TOOL_INPUT_FILE_PATH is the path of the file Claude just wrote. Prettier runs on exactly that file, not your whole project.

Hook 2: Auto-test on write

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "npm test -- --testPathPattern=$(basename $CLAUDE_TOOL_INPUT_FILE_PATH .ts) --passWithNoTests 2>&1 | tail -20"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

This runs tests related to whatever file Claude just edited. If the test fails, Claude sees the failure output and fixes it immediately — no prompting needed.

Hook 3: Type-check after every edit

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "npx tsc --noEmit 2>&1 | head -30"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

TypeScript errors appear after every file write. Claude catches type regressions the moment they happen, not at the end of a long session.

The full self-healing loop

Combine all three:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "npx prettier --write $CLAUDE_TOOL_INPUT_FILE_PATH 2>/dev/null; npx tsc --noEmit 2>&1 | head -20; npm test -- --passWithNoTests 2>&1 | tail -10"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Now every file write triggers:

  1. Prettier formats it
  2. TypeScript checks for type errors
  3. Tests run and output the last 10 lines

Claude reads all of this output and continues fixing until everything is green.

Hook 4: Block dangerous commands

Use PreToolUse to prevent Claude from running commands you don't want:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "echo $CLAUDE_TOOL_INPUT_COMMAND | grep -qE 'rm -rf|DROP TABLE|truncate' && echo 'BLOCKED: dangerous command' && exit 1 || exit 0"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

If the command matches a dangerous pattern, the hook exits with code 1 and Claude sees BLOCKED: dangerous command. The tool call is cancelled.

Hook 5: Notify when Claude finishes

{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude finished\" with title \"Claude Code\"' 2>/dev/null || notify-send 'Claude Code' 'Claude finished' 2>/dev/null || true"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Mac users get a system notification. Linux users get notify-send. You can step away from your desk and come back when Claude is done.

Where to put hooks

Project-level (applies to this repo only):

.claude/settings.json
Enter fullscreen mode Exit fullscreen mode

Global (applies to all Claude Code sessions):

~/.claude/settings.json
Enter fullscreen mode Exit fullscreen mode

Start with project-level. The commands will be different per project — your Python project's linter isn't the same as your Node project's linter.

Practical tip: keep hooks fast

Hooks run synchronously. If your test suite takes 3 minutes, don't run all tests on every write. Run only the test file matching the edited file:

npm test -- --testPathPattern=$(basename $CLAUDE_TOOL_INPUT_FILE_PATH .ts) --passWithNoTests 2>&1 | tail -10
Enter fullscreen mode Exit fullscreen mode

Or run just the linter (fast) on write, and full tests only at Stop:

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

Rate limits break the loop

The self-healing loop depends on Claude staying in the session. If you hit Claude's rate limit mid-session, the loop breaks. You're back to manually running lint and tests.

The fix: set ANTHROPIC_BASE_URL to a proxy that removes rate limits.

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

SimplyLouie is ✌️$2/month. The hooks loop runs uninterrupted — Claude writes, formats, tests, fixes, and finishes without hitting a wall.

Summary

Hook type Use for
PostToolUse + Write matcher Auto-format, auto-test, type-check
PreToolUse + Bash matcher Block dangerous commands
Stop Notifications, full test suite

Hooks turn Claude Code from an AI you prompt into a system that enforces quality automatically. Set them up once, forget about formatting and type errors forever.

Top comments (0)