Claude Code hooks: run scripts automatically before and after every AI edit
Claude Code 1.x shipped a feature most developers haven't found yet: hooks.
Hooks let you run shell commands automatically before Claude edits a file, after it edits a file, or after a session ends. Think of them as git hooks, but for AI edits.
Here's what you can do with them.
The basics: what hooks are
Hooks are shell commands defined in your .claude/settings.json. Claude runs them at specific lifecycle points:
-
PreToolUse— before Claude uses a tool (edit file, run bash, etc) -
PostToolUse— after Claude uses a tool -
Stop— when the session ends
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "npx prettier --write $CLAUDE_TOOL_INPUT_PATH 2>/dev/null || true"
}
]
}
]
}
}
This runs Prettier automatically every time Claude writes or edits a file. You never have to manually format again.
Hook 1: auto-format on every edit
The most useful hook. Every file Claude touches gets formatted immediately:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "npx prettier --write \"$CLAUDE_TOOL_INPUT_PATH\" 2>/dev/null || true"
}
]
}
]
}
}
The || true prevents the hook from blocking Claude if Prettier errors on a non-JS file.
Hook 2: auto-lint and report errors back
Run ESLint after every edit. If there are errors, Claude sees them and fixes them in the same session:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "npx eslint \"$CLAUDE_TOOL_INPUT_PATH\" --format compact 2>&1 | tail -20 || true"
}
]
}
]
}
}
This is powerful because Claude reads the hook output. If ESLint finds a problem, Claude sees it and can fix it in the next step without you doing anything.
Hook 3: run tests after every edit
Auto-run the tests related to the file Claude just changed:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "npm test -- --testPathPattern=$(basename $CLAUDE_TOOL_INPUT_PATH .js) --passWithNoTests 2>&1 | tail -30 || true"
}
]
}
]
}
}
This uses Jest's --testPathPattern to find tests named after the edited file. If tests fail, Claude sees the output and fixes the code.
Hook 4: block dangerous operations
Use PreToolUse to prevent Claude from doing things you don't want:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo \"$CLAUDE_TOOL_INPUT_COMMAND\" | grep -E 'rm -rf|DROP TABLE|DELETE FROM' && echo 'BLOCKED: dangerous command' && exit 1 || true"
}
]
}
]
}
}
If the hook exits with a non-zero status, Claude's tool use is blocked. Claude sees the output and tries a safer approach.
Hook 5: git commit after session ends
Auto-commit all Claude's changes when the session ends:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "cd $CLAUDE_PROJECT_DIR && git diff --quiet || git add -A && git commit -m 'claude: auto-commit session changes' 2>/dev/null || true"
}
]
}
]
}
}
Only commits if there are actual changes. The || true prevents errors if git isn't initialized.
Environment variables available in hooks
Claude exposes these to your hook scripts:
| Variable | Value |
|---|---|
$CLAUDE_TOOL_NAME |
Name of the tool (Write, Edit, Bash, etc) |
$CLAUDE_TOOL_INPUT_PATH |
File path being edited |
$CLAUDE_TOOL_INPUT_COMMAND |
Bash command being run (for Bash tool) |
$CLAUDE_PROJECT_DIR |
Root of your project |
$CLAUDE_SESSION_ID |
Unique ID for this session |
Complete settings.json with all hooks
Here's a production-ready config combining all of the above:
{
"permissions": {
"allow": ["Bash(*)", "Read(*)", "Write(*)", "Edit(*)", "MultiEdit(*)"]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo \"$CLAUDE_TOOL_INPUT_COMMAND\" | grep -qE 'rm -rf /' && echo 'BLOCKED' && exit 1 || true"
}
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "npx prettier --write \"$CLAUDE_TOOL_INPUT_PATH\" 2>/dev/null || true"
},
{
"type": "command",
"command": "npx eslint \"$CLAUDE_TOOL_INPUT_PATH\" --format compact 2>&1 | tail -10 || true"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "cd $CLAUDE_PROJECT_DIR && git diff --quiet || (git add -A && git commit -m 'claude: session end auto-commit') 2>/dev/null || true"
}
]
}
]
}
}
The killer use case: self-healing code
Combine lint hook + test hook and you get something powerful:
- Claude edits a file
- ESLint hook runs → finds an error → Claude sees it
- Claude fixes the error → edits the file again
- Test hook runs → tests fail → Claude sees it
- Claude fixes the test → edits the test file
- Test hook runs again → passes
- Claude reports done
All automatically. You just gave the initial instruction.
One more thing: rate limits
If you're running complex projects with many files, Claude's built-in API rate limits will interrupt your hooks mid-session. The ANTHROPIC_BASE_URL environment variable points Claude Code to a different API endpoint:
export ANTHROPIC_BASE_URL=https://simplylouie.com
SimplyLouie runs at ✌️$2/month and removes the rate limit interruptions. The hooks keep running, the self-healing loop keeps working, no interruptions.
Set it once in your shell profile and forget it.
What hooks are you using? Drop them in the comments — I'll add the best ones to this article.
Top comments (0)