Claude Code hooks: auto-format, auto-test, and self-heal on every save
Claude Code hooks: auto-format, auto-test, and self-heal on every save Claude Code hooks let you run shell commands automatically before or after Claude uses a tool. This means you can build a fully automated loop: Claude edits a file → your linter runs → tests execute → if something breaks, Claude fixes it. Here's exactly how to set it up. What hooks actually do Hooks fire at specific events in Claude Code's tool execution lifecycle: PreToolUse — runs before Claude uses a tool (read file, write file, bash, etc.) PostToolUse — runs after Claude uses a tool Stop — runs when Claude finishes a turn Notification — runs when Claude sends you a notification You configure them in ~/.claude/settings.json (global) or .claude/settings.json (project). The auto-format hook This hook runs Prettier auto
Claude Code hooks: auto-format, auto-test, and self-heal on every save
Claude Code hooks let you run shell commands automatically before or after Claude uses a tool. This means you can build a fully automated loop: Claude edits a file → your linter runs → tests execute → if something breaks, Claude fixes it.
Here's exactly how to set it up.
What hooks actually do
Hooks fire at specific events in Claude Code's tool execution lifecycle:
-
PreToolUse — runs before Claude uses a tool (read file, write file, bash, etc.)
-
PostToolUse — runs after Claude uses a tool
-
Stop — runs when Claude finishes a turn
-
Notification — runs when Claude sends you a notification
You configure them in ~/.claude/settings.json (global) or .claude/settings.json (project).
The auto-format hook
This hook runs Prettier automatically whenever Claude writes a file:
{ "hooks": { "PostToolUse": [ { "matcher": "Write", "hooks": [ { "type": "command", "command": "prettier --write $CLAUDE_TOOL_INPUT_FILE_PATH 2>/dev/null || true" } ] } ] } }{ "hooks": { "PostToolUse": [ { "matcher": "Write", "hooks": [ { "type": "command", "command": "prettier --write $CLAUDE_TOOL_INPUT_FILE_PATH 2>/dev/null || true" } ] } ] } }Enter fullscreen mode
Exit fullscreen mode
$CLAUDE_TOOL_INPUT_FILE_PATH is the environment variable Claude Code sets to the file path being written. Every time Claude edits a file, Prettier runs on it automatically. No manual formatting.
The auto-test hook
Run your test suite after every file write:
{ "hooks": { "PostToolUse": [ { "matcher": "Write", "hooks": [ { "type": "command", "command": "npm test --silent 2>&1 | tail -5" } ] } ] } }{ "hooks": { "PostToolUse": [ { "matcher": "Write", "hooks": [ { "type": "command", "command": "npm test --silent 2>&1 | tail -5" } ] } ] } }Enter fullscreen mode
Exit fullscreen mode
Claude sees the test output. If tests fail, it knows immediately and can fix the file before moving to the next task.
Combining format + test + auto-fix
Here's the full self-healing loop:
{ "hooks": { "PostToolUse": [ { "matcher": "Write", "hooks": [ { "type": "command", "command": "prettier --write $CLAUDE_TOOL_INPUT_FILE_PATH 2>/dev/null || true && npm test --silent 2>&1 | tail -10" } ] } ] } }{ "hooks": { "PostToolUse": [ { "matcher": "Write", "hooks": [ { "type": "command", "command": "prettier --write $CLAUDE_TOOL_INPUT_FILE_PATH 2>/dev/null || true && npm test --silent 2>&1 | tail -10" } ] } ] } }Enter fullscreen mode
Exit fullscreen mode
The flow:
-
Claude writes a file
-
Prettier formats it automatically
-
Tests run
-
If tests fail, Claude sees the output in its context
-
Claude fixes the failure
-
Repeat until tests pass
This turns Claude Code into a proper TDD loop without you manually running anything.
The .env blocker hook
Prevent Claude from reading your secrets file:
{ "hooks": { "PreToolUse": [ { "matcher": "Read", "hooks": [ { "type": "command", "command": "if [[ $CLAUDE_TOOL_INPUT_FILE_PATH == *'.env'* ]]; then echo 'BLOCKED: .env files are not accessible to Claude'; exit 1; fi" } ] } ] } }{ "hooks": { "PreToolUse": [ { "matcher": "Read", "hooks": [ { "type": "command", "command": "if [[ $CLAUDE_TOOL_INPUT_FILE_PATH == *'.env'* ]]; then echo 'BLOCKED: .env files are not accessible to Claude'; exit 1; fi" } ] } ] } }Enter fullscreen mode
Exit fullscreen mode
Exit code 1 from a PreToolUse hook cancels the tool call. Claude gets the error message in its context and won't try again.
The ESLint hook (with auto-fix)
{ "hooks": { "PostToolUse": [ { "matcher": "Write", "hooks": [ { "type": "command", "command": "eslint --fix $CLAUDE_TOOL_INPUT_FILE_PATH 2>&1 | head -20" } ] } ] } }{ "hooks": { "PostToolUse": [ { "matcher": "Write", "hooks": [ { "type": "command", "command": "eslint --fix $CLAUDE_TOOL_INPUT_FILE_PATH 2>&1 | head -20" } ] } ] } }Enter fullscreen mode
Exit fullscreen mode
--fix auto-corrects fixable lint errors. The remaining unfixable ones get printed to stdout, which Claude sees and can address manually.
The Stop hook — run after every Claude turn
Stop hooks fire when Claude finishes responding. Useful for running a full test suite at the end of a session:
{ "hooks": { "Stop": [ { "hooks": [ { "type": "command", "command": "npm test 2>&1 | tail -20" } ] } ] } }{ "hooks": { "Stop": [ { "hooks": [ { "type": "command", "command": "npm test 2>&1 | tail -20" } ] } ] } }Enter fullscreen mode
Exit fullscreen mode
This gives you a final health check at the end of every Claude Code session.
Combining everything in one settings file
{ "hooks": { "PreToolUse": [ { "matcher": "Read", "hooks": [ { "type": "command", "command": "if [[ $CLAUDE_TOOL_INPUT_FILE_PATH == *'.env'* ]]; then echo 'BLOCKED: cannot read .env'; exit 1; fi" } ] } ], "PostToolUse": [ { "matcher": "Write", "hooks": [ { "type": "command", "command": "prettier --write $CLAUDE_TOOL_INPUT_FILE_PATH 2>/dev/null || true && eslint --fix $CLAUDE_TOOL_INPUT_FILE_PATH 2>&1 | head -5 || true && npm test --silent 2>&1 | tail -10" } ] } ], "Stop": [ { "hooks": [ { "type": "command", "command": "echo 'Session complete. Running final test suite...' && npm test 2>&1 | tail -20" } ] } ] } }{ "hooks": { "PreToolUse": [ { "matcher": "Read", "hooks": [ { "type": "command", "command": "if [[ $CLAUDE_TOOL_INPUT_FILE_PATH == *'.env'* ]]; then echo 'BLOCKED: cannot read .env'; exit 1; fi" } ] } ], "PostToolUse": [ { "matcher": "Write", "hooks": [ { "type": "command", "command": "prettier --write $CLAUDE_TOOL_INPUT_FILE_PATH 2>/dev/null || true && eslint --fix $CLAUDE_TOOL_INPUT_FILE_PATH 2>&1 | head -5 || true && npm test --silent 2>&1 | tail -10" } ] } ], "Stop": [ { "hooks": [ { "type": "command", "command": "echo 'Session complete. Running final test suite...' && npm test 2>&1 | tail -20" } ] } ] } }Enter fullscreen mode
Exit fullscreen mode
Why this matters for rate limits
The self-healing loop means Claude finishes tasks faster with fewer back-and-forth turns. Fewer turns = fewer tokens = less chance of hitting rate limits mid-session.
If you're hitting Claude Code rate limits anyway, the pattern is to route through a proxy that removes the limit entirely:
export ANTHROPIC_BASE_URL=https://simplylouie.com
Enter fullscreen mode
Exit fullscreen mode
Set that in your shell profile and claude uses it automatically. Rate limits gone. ✌️$2/month at simplylouie.com.
The matcher field
The matcher field accepts any Claude Code tool name:
-
Write — file writes
-
Read — file reads
-
Bash — bash commands
-
Edit — file edits (alternative to Write in some versions)
-
Leave it empty to match all tools
Summary
-
PostToolUse Write = auto-format + auto-test after every file edit
-
PreToolUse Read = block sensitive files before Claude reads them
-
Stop = full suite at end of session
-
$CLAUDE_TOOL_INPUT_FILE_PATH = the file being written (use in your hook commands)
-
Exit code 1 in PreToolUse = cancels the tool call
Hooks turn Claude Code from a chat interface into a proper CI loop running in your terminal. Once configured, you stop running lint and tests manually entirely.
Dev.to AI
https://dev.to/subprime2010/claude-code-hooks-auto-format-auto-test-and-self-heal-on-every-save-2g1nSign in to highlight and annotate this article

Conversation starters
Daily AI Digest
Get the top 5 AI stories delivered to your inbox every morning.
More about
claudeversionglobal
7 Key Benefits of Conversational AI for Business
Every business owner knows the frustration. A potential customer messages you at 11 PM, doesn’t get a reply, and buys from someone else by morning. That’s revenue lost to silence. Conversational AI fixes this problem and about a dozen others you probably haven’t considered yet. The market agrees. The global conversational AI market is projected to reach USD 41.39 billion by 2030 , growing at a 23.7% CAGR. That kind of growth isn’t hype. It’s businesses voting with their wallets because they see real returns. Whether you run a dental clinic, a car dealership, or a boutique hotel, the benefits of conversational AI for business are hard to ignore. From slashing support costs to turning casual browsers into paying customers, AI-powered conversations are reshaping how companies interact with th
Knowledge Map
Connected Articles — Knowledge Graph
This article is connected to other articles through shared AI topics and tags.




Discussion
Sign in to join the discussion
No comments yet — be the first to share your thoughts!