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 before or after Claude touches any file. This turns Claude into a fully automated engineering loop — not just an editor, but a system that formats, tests, and fixes its own output.

Here's how to set them up and make them actually useful.

What hooks do

Hooks fire at two moments:

  • PreToolUse — before Claude reads or writes a file
  • PostToolUse — after Claude writes a file

You define them in .claude/settings.json at the project root.

Basic setup

Create .claude/settings.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "npm run lint -- --fix $CLAUDE_TOOL_INPUT_FILE_PATH 2>&1 | tail -5"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Now every time Claude writes a file, ESLint auto-fixes it. No more "can you also fix the formatting" follow-ups.

The self-healing loop

The real power is chaining hooks to create a test → fail → fix cycle:

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

Claude writes a file → tests run → if tests fail, Claude sees the output and fixes the code → tests run again → repeat until green.

Auto-lint on write (Python)

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "if [[ $CLAUDE_TOOL_INPUT_FILE_PATH == *.py ]]; then black $CLAUDE_TOOL_INPUT_FILE_PATH && ruff check $CLAUDE_TOOL_INPUT_FILE_PATH --fix; fi"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Black formats, ruff fixes lint issues — all automatically, before you even review Claude's output.

Auto-typecheck on write (TypeScript)

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "if [[ $CLAUDE_TOOL_INPUT_FILE_PATH == *.ts || $CLAUDE_TOOL_INPUT_FILE_PATH == *.tsx ]]; then npx tsc --noEmit 2>&1 | head -20; fi"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

TypeScript errors surface immediately after every write. Claude sees the tsc output and self-corrects.

PreToolUse: validate before Claude reads sensitive files

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

This blocks Claude from reading .env files — useful if you want to keep secrets out of context even when Claude asks.

The complete auto-pilot config

Here's the full .claude/settings.json for a Node.js project:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Read",
        "hooks": [
          {
            "type": "command",
            "command": "if [[ $CLAUDE_TOOL_INPUT_FILE_PATH == *.env* ]]; then echo 'BLOCKED'; exit 1; fi"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "eslint --fix $CLAUDE_TOOL_INPUT_FILE_PATH 2>&1 | tail -3"
          },
          {
            "type": "command",
            "command": "npm test -- --testPathPattern=$(basename $CLAUDE_TOOL_INPUT_FILE_PATH .js) --passWithNoTests 2>&1 | tail -10"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Every file Claude writes gets linted and tested automatically. Failures loop back to Claude for self-correction.

The key env variable

$CLAUDE_TOOL_INPUT_FILE_PATH — the absolute path of the file Claude just wrote. This is what makes targeted hooks possible (run tests only for the file that changed, not the whole suite).

When this matters most

Hooks become essential when you're running Claude on long autonomous tasks — refactoring a module, writing a feature end-to-end, or updating types across many files. Without hooks, Claude writes code and moves on. With hooks, Claude writes → validates → self-corrects, without you intervening.

For heavy Claude Code sessions hitting rate limits: ANTHROPIC_BASE_URL=https://simplylouie.com routes through a $2/month proxy that removes per-session limits. Hooks + no rate limits = fully autonomous sessions.


Have a hooks config that changed your workflow? Drop it in the comments.

Top comments (0)