Claude Code hooks: auto-format, auto-test, and self-heal on every file save
Claude Code hooks let you run shell commands automatically at key moments in your session — before Claude reads a file, after it writes one, or when a tool call completes. This is how you build a self-healing loop where Claude formats, tests, and fixes code without you having to ask.
What are hooks?
Hooks are defined in your .claude/settings.json file. They fire at lifecycle events during Claude's execution.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "npm run lint --silent"
}
]
}
]
}
}
Every time Claude writes or edits a file, this runs npm run lint automatically. Claude sees the output and fixes any lint errors before moving on.
The four hook events
| Event | When it fires |
|---|---|
PreToolUse |
Before Claude calls a tool |
PostToolUse |
After Claude calls a tool |
Notification |
When Claude sends a notification |
Stop |
When Claude finishes responding |
Hook 1: Auto-format on write
This runs Prettier after every file write:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "npx prettier --write $CLAUDE_TOOL_INPUT_FILE_PATH 2>/dev/null || true"
}
]
}
]
}
}
$CLAUDE_TOOL_INPUT_FILE_PATH is the path of the file Claude just wrote. Prettier runs on exactly that file, not your whole project.
Hook 2: Auto-test on write
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "npm test -- --testPathPattern=$(basename $CLAUDE_TOOL_INPUT_FILE_PATH .ts) --passWithNoTests 2>&1 | tail -20"
}
]
}
]
}
}
This runs tests related to whatever file Claude just edited. If the test fails, Claude sees the failure output and fixes it immediately — no prompting needed.
Hook 3: Type-check after every edit
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "npx tsc --noEmit 2>&1 | head -30"
}
]
}
]
}
}
TypeScript errors appear after every file write. Claude catches type regressions the moment they happen, not at the end of a long session.
The full self-healing loop
Combine all three:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "npx prettier --write $CLAUDE_TOOL_INPUT_FILE_PATH 2>/dev/null; npx tsc --noEmit 2>&1 | head -20; npm test -- --passWithNoTests 2>&1 | tail -10"
}
]
}
]
}
}
Now every file write triggers:
- Prettier formats it
- TypeScript checks for type errors
- Tests run and output the last 10 lines
Claude reads all of this output and continues fixing until everything is green.
Hook 4: Block dangerous commands
Use PreToolUse to prevent Claude from running commands you don't want:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo $CLAUDE_TOOL_INPUT_COMMAND | grep -qE 'rm -rf|DROP TABLE|truncate' && echo 'BLOCKED: dangerous command' && exit 1 || exit 0"
}
]
}
]
}
}
If the command matches a dangerous pattern, the hook exits with code 1 and Claude sees BLOCKED: dangerous command. The tool call is cancelled.
Hook 5: Notify when Claude finishes
{
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude finished\" with title \"Claude Code\"' 2>/dev/null || notify-send 'Claude Code' 'Claude finished' 2>/dev/null || true"
}
]
}
]
}
}
Mac users get a system notification. Linux users get notify-send. You can step away from your desk and come back when Claude is done.
Where to put hooks
Project-level (applies to this repo only):
.claude/settings.json
Global (applies to all Claude Code sessions):
~/.claude/settings.json
Start with project-level. The commands will be different per project — your Python project's linter isn't the same as your Node project's linter.
Practical tip: keep hooks fast
Hooks run synchronously. If your test suite takes 3 minutes, don't run all tests on every write. Run only the test file matching the edited file:
npm test -- --testPathPattern=$(basename $CLAUDE_TOOL_INPUT_FILE_PATH .ts) --passWithNoTests 2>&1 | tail -10
Or run just the linter (fast) on write, and full tests only at Stop:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [{"type": "command", "command": "npx prettier --write $CLAUDE_TOOL_INPUT_FILE_PATH 2>/dev/null"}]
}
],
"Stop": [
{
"matcher": "",
"hooks": [{"type": "command", "command": "npm test 2>&1 | tail -20"}]
}
]
}
}
Rate limits break the loop
The self-healing loop depends on Claude staying in the session. If you hit Claude's rate limit mid-session, the loop breaks. You're back to manually running lint and tests.
The fix: set ANTHROPIC_BASE_URL to a proxy that removes rate limits.
export ANTHROPIC_BASE_URL=https://simplylouie.com
claude
SimplyLouie is ✌️$2/month. The hooks loop runs uninterrupted — Claude writes, formats, tests, fixes, and finishes without hitting a wall.
Summary
| Hook type | Use for |
|---|---|
PostToolUse + Write matcher |
Auto-format, auto-test, type-check |
PreToolUse + Bash matcher |
Block dangerous commands |
Stop |
Notifications, full test suite |
Hooks turn Claude Code from an AI you prompt into a system that enforces quality automatically. Set them up once, forget about formatting and type errors forever.
Top comments (0)