Claude Code hooks: auto-format, lint, and test on every save
Claude Code hooks let you run shell commands automatically at specific points in Claude's workflow — before a tool runs, after a file is written, when a session starts or ends.
Most developers discover hooks when they're tired of Claude writing code that doesn't match their formatter settings. Here's how to stop that permanently.
Where hooks live
Hooks go in your CLAUDE.md or in .claude/settings.json at the project root:
{
"hooks": {
"afterFileWrite": "prettier --write $FILE",
"afterFileWrite": "eslint --fix $FILE",
"afterSessionEnd": "npm test -- --passWithNoTests"
}
}
The $FILE variable is the path of the file Claude just wrote. $SESSION_ID is available if you want to log by session.
The four hook points
| Hook | When it fires | Common use |
|---|---|---|
beforeToolRun |
Before any tool executes | Log what Claude is about to do |
afterFileWrite |
After Claude writes or edits a file | Format, lint, type-check |
afterBashRun |
After a bash command completes | Capture output, trigger CI |
afterSessionEnd |
When the session closes | Run test suite, commit |
Auto-format on write
This is the most common hook. Claude writes a file → it gets formatted immediately:
{
"hooks": {
"afterFileWrite": "npx prettier --write $FILE 2>/dev/null || true"
}
}
The || true prevents Claude from seeing a non-zero exit code as an error. If the file isn't JS/TS/CSS, prettier skips it silently.
Auto-lint and auto-fix
{
"hooks": {
"afterFileWrite": "npx eslint --fix $FILE 2>/dev/null || true"
}
}
Problem: you can only have one afterFileWrite hook per settings block. Chain them with &&:
{
"hooks": {
"afterFileWrite": "npx prettier --write $FILE 2>/dev/null; npx eslint --fix $FILE 2>/dev/null; true"
}
}
Auto-test after session
Run your test suite every time Claude finishes a session:
{
"hooks": {
"afterSessionEnd": "npm test -- --passWithNoTests 2>&1 | tail -20"
}
}
Claude sees the test output and can immediately fix failures in the next session.
Self-healing loop pattern
This is where hooks get powerful. Combine afterSessionEnd with a script that reruns Claude if tests fail:
#!/bin/bash
# run-with-tests.sh
npm test 2>&1 > /tmp/test-output.txt
if [ $? -ne 0 ]; then
echo "Tests failed, asking Claude to fix..."
claude --print "Fix the failing tests. Output:\n$(cat /tmp/test-output.txt)" \
--allowedTools Edit,Bash
fi
Then set:
{
"hooks": {
"afterSessionEnd": "./run-with-tests.sh"
}
}
Claude writes code → session ends → tests run → if red, Claude is called again with the failure output → it fixes → tests run again. This loop continues until green (or you hit rate limits).
Rate limits break the loop
The self-healing loop is great until Claude hits its hourly rate limit mid-loop. When that happens:
- The loop stalls
- Your terminal hangs waiting for a Claude response
- You have to manually restart
One way around this: point ANTHROPIC_BASE_URL at a proxy that removes the rate limit ceiling:
export ANTHROPIC_BASE_URL=https://simplylouie.com/api/proxy
npm run claude-loop
SimplyLouie is a $2/month Claude proxy that removes rate limits — useful when you have a self-healing hook loop running overnight.
beforeToolRun: audit what Claude does
If you want a log of every tool Claude runs:
{
"hooks": {
"beforeToolRun": "echo \"$(date): $TOOL_NAME $TOOL_INPUT\" >> ~/.claude-audit.log"
}
}
This creates an append-only audit log at ~/.claude-audit.log. Useful for understanding what Claude actually does in long sessions.
Hooks vs CLAUDE.md
Hooks handle behavior (what happens after actions). CLAUDE.md handles knowledge (what Claude should know and follow).
They work best together:
- CLAUDE.md: "Always use Prettier for formatting, never manual spaces"
- Hook:
afterFileWrite: prettier --write $FILE
CLAUDE.md tells Claude the rule. The hook enforces it automatically even if Claude forgets.
Summary
Hooks are Claude Code's automation layer. The most useful combos:
-
afterFileWrite→ prettier + eslint (keeps code clean without reminders) -
afterSessionEnd→ test suite (catches regressions immediately) -
beforeToolRun→ audit log (understand what's happening in long sessions) -
afterSessionEnd→ self-healing script (autonomous fix loops)
Start with the formatter hook. Once you've run a session without manually formatting anything, you'll add the rest.
Top comments (0)