Run lint and format automatically after every edit with Claude Code hooks
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.
Contents
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.
| Event | Fires | Typical use |
|---|---|---|
PreToolUse | Before a tool runs | Block dangerous commands, pre-checks |
PostToolUse | After a tool runs | Lint / format, tests |
UserPromptSubmit | When you submit a prompt | Add context, validate |
Notification | When Claude asks for input or notifies | Desktop or Slack notifications |
Stop | When Claude finishes a response | Final checks, completion notice |
SubagentStop | When a subagent finishes | Verify subagent output |
SessionStart | At session start | Load env vars, inject context |
PreCompact | Right before context compaction | Save 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
- Run
/hooksand confirm the hook is listed. - Ask Claude to add a deliberately badly formatted function to
src/example.ts. - Prettier should run right after the edit and the file should come out formatted.
- 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 jqon macOS,apt install jqon 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 insettings.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
PostToolUsewithmatcher: "Edit|Write|MultiEdit"to run after edits - Read
tool_input.file_pathfrom 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?
Work through these in order.
- Run
/hooksand check the hook is listed. If not, the JSON is invalid (trailing comma, quotes) or the file is in the wrong place (.claude/settings.jsonor~/.claude/settings.json). matcheris a regular expression on the tool name:Edit|Write|MultiEditfor file edits,Bashfor commands. Check the event name spelling too (PreToolUse/PostToolUse).- Make sure the script is executable (
chmod +x), has a shebang line, and thatjqis installed. Calling it asbash .claude/hooks/format.shavoids the permission issue. - Start with
claude --debugto 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.