From hooks-plugin
Configures Claude Code hooks for lifecycle events like PreToolUse, SessionStart, and automation use cases such as formatting enforcement and permission control.
npx claudepluginhub laurigates/claude-plugins --plugin hooks-pluginThis skill is limited to using the following tools:
Expert knowledge for configuring and developing Claude Code hooks to automate workflows and enforce best practices.
Creates isolated Git worktrees for feature branches with prioritized directory selection, gitignore safety checks, auto project setup for Node/Python/Rust/Go, and baseline verification.
Executes implementation plans in current session by dispatching fresh subagents per independent task, with two-stage reviews: spec compliance then code quality.
Dispatches parallel agents to independently tackle 2+ tasks like separate test failures or subsystems without shared state or dependencies.
Expert knowledge for configuring and developing Claude Code hooks to automate workflows and enforce best practices.
What Are Hooks? Hooks are user-defined shell commands that execute at specific points in Claude Code's lifecycle. Unlike relying on Claude to "decide" to run something, hooks provide deterministic, guaranteed execution.
Why Use Hooks?
| 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 |
| PostToolUseFailure | After tool execution fails | Retry decisions, error handling |
| 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:
Stopfires at the session level when the main agent finishes a response turn.SubagentStopfires when an individual subagent (spawned via the Task tool) completes. UseStopfor session-level notifications; useSubagentStopfor per-task quality gates.
For full schemas, examples, and timeout recommendations for each event, see .claude/rules/hooks-reference.md.
Hooks are configured in settings files:
~/.claude/settings.json - User-level (applies everywhere).claude/settings.json - Project-level (committed to repo).claude/settings.local.json - Local project (not committed)Claude Code merges all matching hooks from all files.
Hooks can also be defined directly in skill and command frontmatter using the hooks field:
---
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
---
This allows skills and commands to define their own hooks that are active only when that skill/command is in use.
{
"hooks": {
"EventName": [
{
"matcher": "ToolPattern",
"hooks": [
{
"type": "command",
"command": "your-command-here",
"timeout": 30
}
]
}
]
}
}
"Bash" - matches exactly "Bash" tool"Edit|Write" - matches either tool"Notebook.*" - matches tools starting with "Notebook""*" - matches everything"mcp__server__tool" - targets MCP server toolsHooks receive JSON via stdin with these common fields:
{
"session_id": "unique-session-id",
"transcript_path": "/path/to/conversation.json",
"cwd": "/current/working/directory",
"permission_mode": "mode",
"hook_event_name": "PreToolUse"
}
PreToolUse additional fields:
{
"tool_name": "Bash",
"tool_input": {
"command": "npm test"
}
}
PostToolUse additional fields:
{
"tool_name": "Bash",
"tool_input": { ... },
"tool_response": { ... }
}
SubagentStart additional fields:
{
"subagent_type": "Explore",
"subagent_prompt": "original prompt text",
"subagent_model": "claude-opus"
}
PreToolUse (wrapped in hookSpecificOutput):
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow|deny|ask",
"permissionDecisionReason": "explanation",
"updatedInput": { "modified": "input" }
}
}
Stop/SubagentStop:
{
"decision": "block",
"reason": "required explanation for continuing"
}
SubagentStart (input modification):
{
"updatedPrompt": "modified prompt text to inject context or modify behavior"
}
SessionStart (wrapped in hookSpecificOutput):
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"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)
CONTEXT="Current branch: $BRANCH\nPending changes:\n$GIT_STATUS"
jq -n --arg ctx "$CONTEXT" '{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": $ctx
}
}'
#!/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\"'"
}
]
}
]
}
}
In addition to command hooks, Claude Code supports LLM-powered hooks for decisions that require judgment.
| Type | How It Works | Default Timeout | Use When |
|---|---|---|---|
command | Runs a shell command, reads stdin, returns exit code | 600s | Deterministic rules (regex, field checks) |
http | Sends hook data to an HTTPS endpoint, reads JSON response | 30s | Remote/centralized policy enforcement |
prompt | Single-turn LLM call (Haiku), returns {ok: true/false} | 30s | Judgment on hook input data alone |
agent | Multi-turn subagent with tool access, returns {ok: true/false} | 60s | Verification needing file/tool access |
async: true: Fire-and-forget for command hooks (non-blocking, exit code ignored)once: true: Run only once per session; subsequent triggers are skipped{
"type": "http",
"url": "https://hooks.example.com/pre-tool-use",
"headers": {
"Authorization": "Bearer ${HOOKS_API_KEY}"
},
"timeout": 30
}
Only HTTPS URLs are allowed. Header values support ${ENV_VAR} expansion.
Prompt and agent hooks work on: PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, Stop, SubagentStop, TaskCompleted, UserPromptSubmit.
SessionStart hooks can write environment variables that persist for the session via CLAUDE_ENV_FILE:
if [ -n "$CLAUDE_ENV_FILE" ]; then
echo "NODE_ENV=development" >> "$CLAUDE_ENV_FILE"
fi
All other events (SessionStart, SessionEnd, PreCompact, etc.) support only command hooks.
{
"type": "prompt",
"prompt": "Evaluate whether all tasks are complete. $ARGUMENTS",
"model": "haiku",
"timeout": 30,
"statusMessage": "Checking completeness..."
}
{
"type": "agent",
"prompt": "Check for TODO/FIXME comments and debugging artifacts in changed files. $ARGUMENTS",
"model": "haiku",
"timeout": 60,
"statusMessage": "Checking implementation quality..."
}
Note: Prefer
commandhooks overagenthooks when the logic is deterministic. For example, test verification (detect changed files → find test runner → run tests) is better as a bash script than an agent hook — it eliminates LLM latency on every invocation.
{"ok": true}
{"ok": false, "reason": "Explanation of what's wrong"}
Stop hooks fire every time Claude finishes responding, including after acting on stop hook feedback. Include this check in Stop hook prompts:
First: if stop_hook_active is true in the input, respond with {"ok": true} immediately.
For the full decision guide on when to use each hook type, see .claude/rules/prompt-agent-hooks.md.
When a PreToolUse hook blocks a command:
| 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 |
Critical: User permission does NOT bypass hooks. Retrying a blocked command will fail again.
When command is legitimately needed:
Example response:
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].
Script Development:
catjq for JSON parsingConfiguration:
$CLAUDE_PROJECT_DIR for portable pathsSecurity:
.env or .git/ directlyVerify hook registration:
/hooks
Enable debug logging:
claude --debug
Test hooks manually:
echo '{"tool_input": {"command": "cat file.txt"}}' | bash your-hook.sh
echo $? # Check exit code
See hooks/README.md for full documentation.