Build event-driven hooks in Claude Code for validation, setup, and automation. Use when you need to validate inputs, check environment state, or automate tasks at specific lifecycle events.
This skill inherits all available tools. When active, it can use any tool Claude has access to.
Hooks are event-driven scripts that execute at specific points in Claude Code's lifecycle. They receive JSON input with session data and event-specific information, enabling validation, environment checks, and automated workflows.
Runs before tool execution. Use for:
Runs after tool completion. Use for:
Runs before processing user input. Use for:
Runs at session initialization. Use for:
Runs at session termination. Use for:
{
"session_id": "unique-session-identifier",
"transcript_path": "/path/to/conversation.json",
"cwd": "/current/working/directory",
"hook_event_name": "PreToolUse|PostToolUse|UserPromptSubmit|SessionStart|SessionEnd"
}
PreToolUse:
{
"tool_name": "Bash",
"tool_input": {
"command": "pytest tests/",
"description": "Run test suite"
}
}
PostToolUse:
{
"tool_name": "Edit",
"tool_input": {
"file_path": "/path/to/file.py",
"old_string": "...",
"new_string": "..."
},
"tool_response": {
"success": true,
"message": "File edited successfully"
}
}
UserPromptSubmit:
{
"prompt": "User's input text here"
}
SessionStart:
{
"source": "startup|resume"
}
SessionEnd:
{
"reason": "user_exit|error|timeout"
}
Always use $CLAUDE_PROJECT_DIR to reference hook scripts. Claude Code sets this environment variable to your project root, ensuring hooks work regardless of the current working directory.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-style.sh"
}
]
}
]
}
}
Why this matters:
./scripts/hook.sh become fragile$CLAUDE_PROJECT_DIR always points to your project rootNote: The environment variable is only available when Claude Code spawns the hook command.
Add hooks to .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/scripts/pre-tool-hook.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/scripts/validate-edits.sh"
}
]
}
]
}
}
* - Match all toolsEdit - Match specific toolEdit|Write - Match multiple toolsBash(git:*) - Match tool with patternUse case: Warn if working directory is dirty before file operations
#!/bin/bash
# scripts/pre-tool-git-check.sh
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
if [[ "$TOOL_NAME" == "Edit" || "$TOOL_NAME" == "Write" ]]; then
if ! git diff-index --quiet HEAD --; then
echo "⚠️ Warning: Uncommitted changes in working directory"
echo "Consider committing before editing files"
fi
fi
exit 0 # Don't block, just warn
Configuration:
{
"PreToolUse": [{
"matcher": "Edit|Write",
"hooks": [{"type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/scripts/pre-tool-git-check.sh"}]
}]
}
Use case: Auto-format Python files after editing
#!/bin/bash
# scripts/post-edit-format.sh
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path')
if [[ "$TOOL_NAME" == "Edit" && "$FILE_PATH" == *.py ]]; then
black "$FILE_PATH" --quiet
echo "✅ Formatted $FILE_PATH with black"
fi
exit 0
Use case: Verify dependencies exist before starting
#!/bin/bash
# scripts/session-start-check.sh
MISSING=()
command -v python >/dev/null || MISSING+=("python")
command -v git >/dev/null || MISSING+=("git")
command -v jq >/dev/null || MISSING+=("jq")
if [ ${#MISSING[@]} -gt 0 ]; then
echo "❌ Missing dependencies: ${MISSING[*]}"
exit 1 # Block session
fi
echo "✅ All dependencies available"
exit 0
$CLAUDE_PROJECT_DIR for hook script paths"$CLAUDE_PROJECT_DIR"/scripts/hook.sh./scripts/hook.sh (breaks if CWD changes)/home/user/project/... (not portable)jq for robust JSON parsing# Check for uncommitted changes
git diff-index --quiet HEAD --
# Get current branch
git branch --show-current
# Check if file is tracked
git ls-files --error-unmatch "$FILE_PATH"
# Python
pylint "$FILE_PATH" --score=no --msg-template='{msg_id}: {msg}'
# JavaScript
eslint "$FILE_PATH" --format=compact
# Go
golint "$FILE_PATH"
# Run tests related to changed file
pytest "tests/test_${FILENAME}" --quiet
# Fast syntax check only
python -m py_compile "$FILE_PATH"
❌ Don't: Use relative or absolute paths for hook commands
$CLAUDE_PROJECT_DIR for portable, reliable paths❌ Don't: Use hooks for long-running tasks
❌ Don't: Block on non-critical checks
❌ Don't: Parse JSON with string manipulation
jq for reliable parsing❌ Don't: Match all tools without filtering
❌ Don't: Ignore hook failures silently
man jq or https://jqlang.github.io/jq/