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"
}
]
}
]
}
}
$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"
}
]
}
]
}
}
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"
}
]
}
]
}
}
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"
}
]
}
]
}
}
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"
}
]
}
]
}
}
--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"
}
]
}
]
}
}
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"
}
]
}
]
}
}
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
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.
Top comments (0)