When working with Claude Code, running formatters (mix format, prettier, rubocop) and linters manually after every AI edit gets tedious quickly.

Claude Code includes a Hooks mechanism that runs specified shell commands automatically at key points in the workflow—such as immediately after a tool finishes editing a file.

Here is how to set them up and how I use them in my projects.

Configuration Scopes

Hooks are configured in Claude Code's settings JSON files. There are three levels of scope:

  • ~/.claude/settings.json — Global user settings (applied across all projects)
  • .claude/settings.json — Project settings (committed to git, shared with teammates)
  • .claude/settings.local.json — Local project overrides (gitignored, personal machine only)

Basic Setup: Format on File Edit

To run a formatter like mix format every time Claude Code modifies a file, configure a PostToolUse hook with a matcher for the Edit tool:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "hooks": [
          {
            "type": "command",
            "command": "mix format"
          }
        ]
      }
    ]
  }
}

With this in .claude/settings.json, you no longer have to worry about unformatted code making it into your diffs.

Trigger Points and Matchers

Claude Code provides several trigger points:

  • PreToolUse: Runs before a tool executes. Useful for logging or validation.
  • PostToolUse: Runs after a tool completes successfully. Best for formatters and linters.
  • Stop: Runs when Claude finishes responding to the prompt.
  • Notification: Triggers when Claude sends a notification.

The matcher field controls which tool triggers the hook:

  • "matcher": "Edit": Triggers only on file edits.
  • "matcher": "Edit|Write": Regex matching multiple tools (e.g., both partial edits and whole file writes).
  • "matcher": "Bash": Triggers on command executions.
  • Omitted matcher: Runs on every event for that trigger point.

Multi-Command Example

You can chain multiple commands under a single trigger. For example, running both a formatter and a strict linter on every file modification, plus logging shell commands:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "mix format"
          },
          {
            "type": "command",
            "command": "mix credo --strict"
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "echo \"[$(date)] Running: $CLAUDE_TOOL_INPUT\" >> .claude/command_log.txt"
          }
        ]
      }
    ]
  }
}

Summary

Setting up a simple PostToolUse hook for your project's default formatter takes two minutes and removes the friction of manual cleanup after AI-assisted edits. If you work across multiple languages, putting language-agnostic preferences in user settings and project-specific linters in .claude/settings.json keeps things clean and predictable.

Epona
Written byEpona

There's nothing wrong with having a little fun

x.com/simura_epona

Loading comments…