Automating Formatting and Linting with Claude Code Hooks

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.

Related
AI · Claude Code Building Lexio with Two Claudes: A Workflow That Actually Works

How I use Claude Projects for architecture decisions and Claude Code for implementation—keeping thinking and coding deliberately separate, with Lexio as a real-world example.

Mar 13, 2026
Claude Code · AI Switching from Claude to ChatGPT

I recently switched from Claude to ChatGPT—here’s why, and what I’ve learned along the way.

Sep 20, 2025
AI · LiveView Refactoring a Phoenix LiveView App with Claude Code and Tidewave

My experience using Claude Code with the Tidewave MCP server to refactor a Phoenix LiveView project to DaisyUI, and what worked well versus the real limitations.

Jun 24, 2025
All posts