DEV Community

brian austin
brian austin

Posted on

Claude Code hooks: run scripts automatically before and after every AI edit

Claude Code hooks: run scripts automatically before and after every AI edit

Claude Code 1.x shipped a feature most developers haven't found yet: hooks.

Hooks let you run shell commands automatically before Claude edits a file, after it edits a file, or after a session ends. Think of them as git hooks, but for AI edits.

Here's what you can do with them.

The basics: what hooks are

Hooks are shell commands defined in your .claude/settings.json. Claude runs them at specific lifecycle points:

  • PreToolUse — before Claude uses a tool (edit file, run bash, etc)
  • PostToolUse — after Claude uses a tool
  • Stop — when the session ends
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "npx prettier --write $CLAUDE_TOOL_INPUT_PATH 2>/dev/null || true"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

This runs Prettier automatically every time Claude writes or edits a file. You never have to manually format again.

Hook 1: auto-format on every edit

The most useful hook. Every file Claude touches gets formatted immediately:

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

The || true prevents the hook from blocking Claude if Prettier errors on a non-JS file.

Hook 2: auto-lint and report errors back

Run ESLint after every edit. If there are errors, Claude sees them and fixes them in the same session:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "npx eslint \"$CLAUDE_TOOL_INPUT_PATH\" --format compact 2>&1 | tail -20 || true"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

This is powerful because Claude reads the hook output. If ESLint finds a problem, Claude sees it and can fix it in the next step without you doing anything.

Hook 3: run tests after every edit

Auto-run the tests related to the file Claude just changed:

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

This uses Jest's --testPathPattern to find tests named after the edited file. If tests fail, Claude sees the output and fixes the code.

Hook 4: block dangerous operations

Use PreToolUse to prevent Claude from doing things you don't want:

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

If the hook exits with a non-zero status, Claude's tool use is blocked. Claude sees the output and tries a safer approach.

Hook 5: git commit after session ends

Auto-commit all Claude's changes when the session ends:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "cd $CLAUDE_PROJECT_DIR && git diff --quiet || git add -A && git commit -m 'claude: auto-commit session changes' 2>/dev/null || true"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Only commits if there are actual changes. The || true prevents errors if git isn't initialized.

Environment variables available in hooks

Claude exposes these to your hook scripts:

Variable Value
$CLAUDE_TOOL_NAME Name of the tool (Write, Edit, Bash, etc)
$CLAUDE_TOOL_INPUT_PATH File path being edited
$CLAUDE_TOOL_INPUT_COMMAND Bash command being run (for Bash tool)
$CLAUDE_PROJECT_DIR Root of your project
$CLAUDE_SESSION_ID Unique ID for this session

Complete settings.json with all hooks

Here's a production-ready config combining all of the above:

{
  "permissions": {
    "allow": ["Bash(*)", "Read(*)", "Write(*)", "Edit(*)", "MultiEdit(*)"]
  },
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "echo \"$CLAUDE_TOOL_INPUT_COMMAND\" | grep -qE 'rm -rf /' && echo 'BLOCKED' && exit 1 || true"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "npx prettier --write \"$CLAUDE_TOOL_INPUT_PATH\" 2>/dev/null || true"
          },
          {
            "type": "command",
            "command": "npx eslint \"$CLAUDE_TOOL_INPUT_PATH\" --format compact 2>&1 | tail -10 || true"
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "cd $CLAUDE_PROJECT_DIR && git diff --quiet || (git add -A && git commit -m 'claude: session end auto-commit') 2>/dev/null || true"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

The killer use case: self-healing code

Combine lint hook + test hook and you get something powerful:

  1. Claude edits a file
  2. ESLint hook runs → finds an error → Claude sees it
  3. Claude fixes the error → edits the file again
  4. Test hook runs → tests fail → Claude sees it
  5. Claude fixes the test → edits the test file
  6. Test hook runs again → passes
  7. Claude reports done

All automatically. You just gave the initial instruction.

One more thing: rate limits

If you're running complex projects with many files, Claude's built-in API rate limits will interrupt your hooks mid-session. The ANTHROPIC_BASE_URL environment variable points Claude Code to a different API endpoint:

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

SimplyLouie runs at ✌️$2/month and removes the rate limit interruptions. The hooks keep running, the self-healing loop keeps working, no interruptions.

Set it once in your shell profile and forget it.


What hooks are you using? Drop them in the comments — I'll add the best ones to this article.

Top comments (0)