Claude Code hooks: auto-format, auto-test, and self-heal on every file save
Claude Code hooks let you run shell commands automatically before or after Claude touches any file. This turns Claude into a fully automated engineering loop — not just an editor, but a system that formats, tests, and fixes its own output.
Here's how to set them up and make them actually useful.
What hooks do
Hooks fire at two moments:
- PreToolUse — before Claude reads or writes a file
- PostToolUse — after Claude writes a file
You define them in .claude/settings.json at the project root.
Basic setup
Create .claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "npm run lint -- --fix $CLAUDE_TOOL_INPUT_FILE_PATH 2>&1 | tail -5"
}
]
}
]
}
}
Now every time Claude writes a file, ESLint auto-fixes it. No more "can you also fix the formatting" follow-ups.
The self-healing loop
The real power is chaining hooks to create a test → fail → fix cycle:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "cd $(dirname $CLAUDE_TOOL_INPUT_FILE_PATH) && npm test -- --testPathPattern=$(basename $CLAUDE_TOOL_INPUT_FILE_PATH .js) 2>&1 | tail -20"
}
]
}
]
}
}
Claude writes a file → tests run → if tests fail, Claude sees the output and fixes the code → tests run again → repeat until green.
Auto-lint on write (Python)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "if [[ $CLAUDE_TOOL_INPUT_FILE_PATH == *.py ]]; then black $CLAUDE_TOOL_INPUT_FILE_PATH && ruff check $CLAUDE_TOOL_INPUT_FILE_PATH --fix; fi"
}
]
}
]
}
}
Black formats, ruff fixes lint issues — all automatically, before you even review Claude's output.
Auto-typecheck on write (TypeScript)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "if [[ $CLAUDE_TOOL_INPUT_FILE_PATH == *.ts || $CLAUDE_TOOL_INPUT_FILE_PATH == *.tsx ]]; then npx tsc --noEmit 2>&1 | head -20; fi"
}
]
}
]
}
}
TypeScript errors surface immediately after every write. Claude sees the tsc output and self-corrects.
PreToolUse: validate before Claude reads sensitive files
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read",
"hooks": [
{
"type": "command",
"command": "if [[ $CLAUDE_TOOL_INPUT_FILE_PATH == *.env* ]]; then echo 'BLOCKED: .env files excluded from Claude context'; exit 1; fi"
}
]
}
]
}
}
This blocks Claude from reading .env files — useful if you want to keep secrets out of context even when Claude asks.
The complete auto-pilot config
Here's the full .claude/settings.json for a Node.js project:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read",
"hooks": [
{
"type": "command",
"command": "if [[ $CLAUDE_TOOL_INPUT_FILE_PATH == *.env* ]]; then echo 'BLOCKED'; exit 1; fi"
}
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "eslint --fix $CLAUDE_TOOL_INPUT_FILE_PATH 2>&1 | tail -3"
},
{
"type": "command",
"command": "npm test -- --testPathPattern=$(basename $CLAUDE_TOOL_INPUT_FILE_PATH .js) --passWithNoTests 2>&1 | tail -10"
}
]
}
]
}
}
Every file Claude writes gets linted and tested automatically. Failures loop back to Claude for self-correction.
The key env variable
$CLAUDE_TOOL_INPUT_FILE_PATH — the absolute path of the file Claude just wrote. This is what makes targeted hooks possible (run tests only for the file that changed, not the whole suite).
When this matters most
Hooks become essential when you're running Claude on long autonomous tasks — refactoring a module, writing a feature end-to-end, or updating types across many files. Without hooks, Claude writes code and moves on. With hooks, Claude writes → validates → self-corrects, without you intervening.
For heavy Claude Code sessions hitting rate limits: ANTHROPIC_BASE_URL=https://simplylouie.com routes through a $2/month proxy that removes per-session limits. Hooks + no rate limits = fully autonomous sessions.
Have a hooks config that changed your workflow? Drop it in the comments.
Top comments (0)