How to configure Claude Code hooks to run shell commands before or after tool calls. Automate linting, testing, notifications, and logging on every Claude Code action.
Claude Code hooks let you attach shell commands to tool lifecycle events — auto-lint on every edit, block writes to protected files, log all commands to audit trail, or ping a webhook when a session completes.
// ~/.claude/settings.json or .claude/settings.json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit", // tool name to match (or "*" for all)
"hooks": [
{
"type": "command",
"command": "npm run lint -- --fix $CLAUDE_TOOL_INPUT_FILE_PATH"
}
]
}
],
"Stop": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "echo 'Claude Code session ended' >> ~/.claude/session.log"
}
]
}
]
}
}
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": "cd $CLAUDE_PROJECT_ROOT && npx eslint --fix "$CLAUDE_TOOL_INPUT_FILE_PATH" 2>&1 | head -20"
}
]
},
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "cd $CLAUDE_PROJECT_ROOT && npx prettier --write "$CLAUDE_TOOL_INPUT_FILE_PATH""
}
]
}
]
}
}
// save as .claude/hooks/guard-migrations.sh
#!/bin/bash
# Exit 2 = block the tool call and show stderr to Claude
FILE="$CLAUDE_TOOL_INPUT_FILE_PATH"
if [[ "$FILE" == *"/migrations/"* ]]; then
echo "BLOCKED: Do not auto-edit migration files. Create a new migration instead." >&2
exit 2
fi
exit 0
// settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit",
"hooks": [{"type": "command", "command": "bash .claude/hooks/guard-migrations.sh"}]
},
{
"matcher": "Write",
"hooks": [{"type": "command", "command": "bash .claude/hooks/guard-migrations.sh"}]
}
]
}
}
// Run the test file matching the edited source file
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": "bash -c 'TEST=$(echo "$CLAUDE_TOOL_INPUT_FILE_PATH" | sed "s/src\//tests\//;s/\.py$/_test.py/"); [ -f "$TEST" ] && python -m pytest "$TEST" -x -q 2>&1 | tail -10 || true'"
}
]
}
]
}
}
// Append timestamp + tool + args to audit log
{
"hooks": {
"PreToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) TOOL=$CLAUDE_TOOL_NAME INPUT=$(echo $CLAUDE_TOOL_INPUT | head -c 200)" >> ~/.claude/audit.log"
}
]
}
]
}
}
| Variable | Value | Available on |
|---|---|---|
| CLAUDE_TOOL_NAME | Name of the tool being called (Edit, Bash, Read…) | All PreToolUse / PostToolUse |
| CLAUDE_TOOL_INPUT | JSON-encoded tool input (full args) | All PreToolUse / PostToolUse |
| CLAUDE_TOOL_INPUT_FILE_PATH | file_path arg (when tool has one) | Edit, Write, Read |
| CLAUDE_TOOL_OUTPUT | Tool output (PostToolUse only) | PostToolUse |
| CLAUDE_PROJECT_ROOT | Absolute path to the project directory | All events |
| CLAUDE_SESSION_ID | Unique ID for this Claude Code session | All events |
| Exit code | Behavior |
|---|---|
| 0 | Success — proceed normally |
| 1 | Non-fatal warning — proceed but show stderr to user |
| 2 (PreToolUse only) | BLOCK — cancel the tool call, show stderr as feedback to Claude |