Run lint and format automatically after every edit with Claude Code hooks

Claude Code Published:

Use Claude Code hooks to run Prettier, ESLint or ruff after every edit: PostToolUse configuration, reading the edited path from stdin, and feeding failures back to Claude.

Verified on Sep 7, 2026 These tools change quickly. Please also check the latest official documentation.
Contents
  1. How hooks work
  2. Example: run Prettier and ESLint after edits
  3. Python projects (ruff)
  4. Try it
  5. Things to watch
  6. Going further: block dangerous commands with PreToolUse
  7. Summary

Writing "format the code before committing" in CLAUDE.md is not enough: Claude sometimes forgets, because instructions are followed probabilistically. Hooks solve this. A hook is a shell command that Claude Code itself runs at a fixed moment, with no model judgement in between.

This article sets up a hook that runs Prettier and ESLint (or ruff for Python) after every file edit, and makes Claude fix whatever fails.

KEY POINT

What you will learn

  • The hook events and how the configuration file is written
  • How to run lint / format only on the file that was edited
  • How to feed lint failures back to Claude so it fixes them

How hooks work

Hooks live under the hooks key of settings.json. For each event you specify which tools to react to (matcher) and the command to run.

EventFiresTypical use
PreToolUseBefore a tool runsBlock dangerous commands, pre-checks
PostToolUseAfter a tool runsLint / format, tests
UserPromptSubmitWhen you submit a promptAdd context, validate
NotificationWhen Claude asks for input or notifiesDesktop or Slack notifications
StopWhen Claude finishes a responseFinal checks, completion notice
SubagentStopWhen a subagent finishesVerify subagent output
SessionStartAt session startLoad env vars, inject context
PreCompactRight before context compactionSave important state

The hook receives JSON on standard input. For PostToolUse it contains tool_name and tool_input (including file_path for edits).

Example: run Prettier and ESLint after edits

Put this in .claude/settings.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/format.sh"
          }
        ]
      }
    ]
  }
}

matcher is a regular expression on the tool name. The tools that change files are Edit, Write and MultiEdit.

Now create .claude/hooks/format.sh. It reads the edited path from the JSON on stdin and formats only that file.

#!/usr/bin/env bash
set -euo pipefail

# Pull file_path out of the stdin JSON (requires jq)
file=$(jq -r '.tool_input.file_path // empty')
[ -z "$file" ] && exit 0
[ -f "$file" ] || exit 0

case "$file" in
  *.ts|*.tsx|*.js|*.jsx|*.json|*.css|*.md)
    npx prettier --write "$file" >/dev/null
    ;;
esac

case "$file" in
  *.ts|*.tsx|*.js|*.jsx)
    if ! npx eslint "$file"; then
      echo "ESLint reported errors. Fix the issues above in: $file" >&2
      exit 2
    fi
    ;;
esac
exit 0
chmod +x .claude/hooks/format.sh

用語解説

Exit codes: 0 is success. 2 means "block": stderr is handed to Claude, which acts on it. Any other code shows a warning to the user without stopping the run.

Python projects (ruff)

#!/usr/bin/env bash
set -euo pipefail
file=$(jq -r '.tool_input.file_path // empty')
[[ "$file" == *.py ]] || exit 0
[ -f "$file" ] || exit 0

ruff format "$file" >/dev/null
if ! ruff check "$file"; then
  echo "Fix the ruff findings in: $file" >&2
  exit 2
fi

With ruff check --fix the hook repairs what it can and only the remaining errors reach Claude.

Try it

  1. Run /hooks and confirm the hook is listed.
  2. Ask Claude to add a deliberately badly formatted function to src/example.ts.
  3. Prettier should run right after the edit and the file should come out formatted.
  4. Ask for code that trips ESLint (an unused variable) and watch Claude fix it after the feedback.

Start Claude Code with claude --debug to see detailed hook logs.

Things to watch

Hooks run on every edit

PostToolUse fires after each edit. A project-wide lint (eslint .) can take tens of seconds every time. Always restrict the hook to the edited file.

  • Be careful overwriting Claude's edits from a hook. Formatters are fine; generating code or moving files in a hook makes Claude's view of the files diverge from reality.
  • jq is required. brew install jq on macOS, apt install jq on Ubuntu. You can parse the JSON with Python instead if you prefer.
  • Sharing with the team: commit the hook scripts and reference them with relative paths from the shared settings.json. Personal hooks go in settings.local.json.

Going further: block dangerous commands with PreToolUse

The same mechanism can inspect commands before they run.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/guard.sh"
          }
        ]
      }
    ]
  }
}
#!/usr/bin/env bash
cmd=$(jq -r '.tool_input.command // empty')
if echo "$cmd" | grep -Eq 'rm -rf /|git push --force|DROP TABLE'; then
  echo "This command is blocked by hooks: $cmd" >&2
  exit 2
fi
exit 0

Combine this with permission deny rules for two layers of protection; see Claude Code permissions in settings.json.

Summary

  • Hooks run deterministically, without the model's judgement, which makes them ideal for lint and format
  • Use PostToolUse with matcher: "Edit|Write|MultiEdit" to run after edits
  • Read tool_input.file_path from the stdin JSON and process only that file
  • Exit code 2 plus stderr tells Claude what to fix

FAQ

Can Claude decide to skip a hook?
No. Hooks are executed deterministically by Claude Code itself, not by the model, so they cannot be forgotten or ignored.
Does Claude learn about a failed hook?
Exit with code 2 and the hook's stderr is fed back to Claude, which then tries to fix the problem. Exit 0 means success; any other code shows a warning to the user.
Where are hooks configured?
In the hooks key of settings.json, at the user, project or local scope. The /hooks command lets you add them interactively.

Questions & answers

Q. My hook is in settings.json but never runs. What should I check?

Answer by the editorsFrequently searched question

Work through these in order.

  1. Run /hooks and check the hook is listed. If not, the JSON is invalid (trailing comma, quotes) or the file is in the wrong place (.claude/settings.json or ~/.claude/settings.json).
  2. matcher is a regular expression on the tool name: Edit|Write|MultiEdit for file edits, Bash for commands. Check the event name spelling too (PreToolUse / PostToolUse).
  3. Make sure the script is executable (chmod +x), has a shebang line, and that jq is installed. Calling it as bash .claude/hooks/format.sh avoids the permission issue.
  4. Start with claude --debug to see each hook's execution, exit code and stderr.

Restart the session after editing settings to be sure the change is loaded.

Primary sources

This article was drafted by AI from official documentation and reviewed by the site operator before publishing. Found a mistake? Let us know via the contact page.