Loading...
Loading...
Claude Code hooks configuration and development. Covers hook lifecycle events, configuration patterns, input/output schemas, and common automation use cases. Use when user mentions hooks, automation, PreToolUse, PostToolUse, SessionStart, SubagentStart, PermissionRequest, WorktreeCreate, WorktreeRemove, TeammateIdle, TaskCompleted, ConfigChange, or needs to enforce consistent behavior in Claude Code workflows.
npx skill4agent add laurigates/claude-plugins hooks-configuration| Event | When It Fires | Key Use Cases |
|---|---|---|
| SessionStart | Session begins/resumes | Environment setup, context loading |
| SessionEnd | Session terminates | Cleanup, state persistence |
| UserPromptSubmit | User submits prompt | Input validation, context injection |
| PreToolUse | Before tool execution | Permission control, blocking dangerous ops |
| PostToolUse | After tool completes | Auto-formatting, logging, validation |
| PermissionRequest | Claude requests permission for a tool | Auto approve/deny without user prompt |
| Stop | Main agent finishes responding | Notifications, git reminders |
| SubagentStart | Subagent (Task tool) is about to start | Input modification, context injection |
| SubagentStop | Subagent finishes | Per-task completion evaluation |
| WorktreeCreate | New git worktree created via EnterWorktree | Worktree setup, dependency install |
| WorktreeRemove | Worktree removed after session exits | Cleanup, uncommitted changes alert |
| TeammateIdle | Teammate in agent team goes idle | Assign additional tasks to teammate |
| TaskCompleted | Task in shared task list marked complete | Validation gates before task acceptance |
| PreCompact | Before context compaction | Transcript backup |
| Notification | Claude sends notification | Custom alerts |
| ConfigChange | Claude Code settings change at runtime | Audit config changes, validation |
Stop vs SubagentStop:fires at the session level when the main agent finishes a response turn.Stopfires when an individual subagent (spawned via the Task tool) completes. UseSubagentStopfor session-level notifications; useStopfor per-task quality gates.SubagentStop
~/.claude/settings.json.claude/settings.json.claude/settings.local.jsonhooks---
name: my-skill
description: A skill with hooks
allowed-tools: Bash, Read
hooks:
PreToolUse:
- matcher: "Bash"
hooks:
- type: command
command: "echo 'Pre-tool hook from skill'"
timeout: 10
---{
"hooks": {
"EventName": [
{
"matcher": "ToolPattern",
"hooks": [
{
"type": "command",
"command": "your-command-here",
"timeout": 30
}
]
}
]
}
}"Bash""Edit|Write""Notebook.*""*""mcp__server__tool"{
"session_id": "unique-session-id",
"transcript_path": "/path/to/conversation.json",
"cwd": "/current/working/directory",
"permission_mode": "mode",
"hook_event_name": "PreToolUse"
}{
"tool_name": "Bash",
"tool_input": {
"command": "npm test"
}
}{
"tool_name": "Bash",
"tool_input": { ... },
"tool_response": { ... }
}{
"subagent_type": "Explore",
"subagent_prompt": "original prompt text",
"subagent_model": "claude-opus"
}{
"permissionDecision": "allow|deny|ask",
"permissionDecisionReason": "explanation",
"updatedInput": { "modified": "input" }
}{
"decision": "block",
"reason": "required explanation for continuing"
}{
"updatedPrompt": "modified prompt text to inject context or modify behavior"
}{
"additionalContext": "Information to inject into session"
}#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
# Block rm -rf /
if echo "$COMMAND" | grep -Eq 'rm\s+(-rf|-fr)\s+/'; then
echo "BLOCKED: Refusing to run destructive command on root" >&2
exit 2
fi
exit 0#!/bin/bash
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
if [[ "$FILE" == *.py ]]; then
ruff format "$FILE" 2>/dev/null
ruff check --fix "$FILE" 2>/dev/null
elif [[ "$FILE" == *.ts ]] || [[ "$FILE" == *.tsx ]]; then
prettier --write "$FILE" 2>/dev/null
fi
exit 0#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if echo "$COMMAND" | grep -Eq '^\s*cat\s+[^|><]'; then
echo "REMINDER: Use the Read tool instead of 'cat'" >&2
exit 2
fi
exit 0#!/bin/bash
GIT_STATUS=$(git status --short 2>/dev/null | head -5)
BRANCH=$(git branch --show-current 2>/dev/null)
cat << EOF
{
"additionalContext": "Current branch: $BRANCH\nPending changes:\n$GIT_STATUS"
}
EOF#!/bin/bash
INPUT=$(cat)
SUBAGENT_TYPE=$(echo "$INPUT" | jq -r '.subagent_type // empty')
ORIGINAL_PROMPT=$(echo "$INPUT" | jq -r '.subagent_prompt // empty')
# Add project context to Explore agents
if [ "$SUBAGENT_TYPE" = "Explore" ]; then
PROJECT_INFO="Project uses TypeScript with Bun. Main source in src/."
cat << EOF
{
"updatedPrompt": "$PROJECT_INFO\n\n$ORIGINAL_PROMPT"
}
EOF
fi
exit 0#!/bin/bash
# Linux
notify-send "Claude Code" "Task completed" 2>/dev/null
# macOS
osascript -e 'display notification "Task completed" with title "Claude Code"' 2>/dev/null
exit 0#!/bin/bash
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // "N/A"')
echo "$(date -Iseconds) | $TOOL | $COMMAND" >> ~/.claude/audit.log
exit 0#!/bin/bash
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
# Auto-approve read-only git operations
if [ "$TOOL" = "Bash" ] && echo "$COMMAND" | grep -Eq '^git (status|log|diff|branch|remote)'; then
echo '{"decision": "approve", "reason": "Read-only git operation"}'
exit 0
fi
# Auto-deny destructive operations on root
if [ "$TOOL" = "Bash" ] && echo "$COMMAND" | grep -Eq 'rm\s+(-rf|-fr)\s+/'; then
echo '{"decision": "deny", "reason": "Destructive root operation blocked"}'
exit 0
fi
exit 0#!/bin/bash
INPUT=$(cat)
WORKTREE_PATH=$(echo "$INPUT" | jq -r '.worktree_path')
if [ -f "$WORKTREE_PATH/package.json" ]; then
(cd "$WORKTREE_PATH" && bun install --frozen-lockfile) 2>/dev/null
fi
exit 0#!/bin/bash
INPUT=$(cat)
TASK_TITLE=$(echo "$INPUT" | jq -r '.task_title')
if echo "$TASK_TITLE" | grep -qi 'implement\|add\|fix\|refactor'; then
if ! npm test --bail 2>/dev/null; then
echo '{"decision": "block", "reason": "Tests must pass before task is accepted."}'
exit 0
fi
fi
exit 0{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash $CLAUDE_PROJECT_DIR/hooks-plugin/hooks/bash-antipatterns.sh",
"timeout": 5
}
]
}
]
}
}{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "bash -c 'FILE=$(cat | jq -r \".tool_input.file_path\"); [[ \"$FILE\" == *.py ]] && ruff format \"$FILE\"'"
}
]
}
]
}
}{
"hooks": {
"Stop": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "bash -c 'changes=$(git status --porcelain | wc -l); [ $changes -gt 0 ] && echo \"Reminder: $changes uncommitted changes\"'"
}
]
}
]
}
}| Type | How It Works | Default Timeout | Use When |
|---|---|---|---|
| Runs a shell command, reads stdin, returns exit code | 600s | Deterministic rules (regex, field checks) |
| Single-turn LLM call (Haiku), returns | 30s | Judgment on hook input data alone |
| Multi-turn subagent with tool access, returns | 60s | Verification needing file/tool access |
PreToolUsePostToolUsePostToolUseFailurePermissionRequestStopSubagentStopTaskCompletedUserPromptSubmitSessionStartSessionEndPreCompactcommand{
"type": "prompt",
"prompt": "Evaluate whether all tasks are complete. $ARGUMENTS",
"model": "haiku",
"timeout": 30,
"statusMessage": "Checking completeness..."
}{
"type": "agent",
"prompt": "Run the test suite and verify all tests pass. $ARGUMENTS",
"model": "haiku",
"timeout": 120,
"statusMessage": "Running test verification..."
}{"ok": true}
{"ok": false, "reason": "Explanation of what's wrong"}First: if stop_hook_active is true in the input, respond with {"ok": true} immediately.| Situation | Action |
|---|---|
| Hook suggests alternative | Use the suggested tool/approach |
| Alternative won't work | Ask user to run command manually |
| User says "proceed" | Still blocked - explain and provide command for manual execution |
The hook blocked `git reset --hard abc123` because it's usually unnecessary.
I considered:
- `git pull`: Won't work because we need to discard local commits, not merge
- `git restore`: Only handles file changes, not commit history
If you need to proceed, please run manually:
git reset --hard abc123
This is needed because [specific justification].catjq$CLAUDE_PROJECT_DIR.env.git//hooksclaude --debugecho '{"tool_input": {"command": "cat file.txt"}}' | bash your-hook.sh
echo $? # Check exit codehooks/README.md