Help us improve
Share bugs, ideas, or general feedback.
From claude-code-expert
Documents Claude Code hooks lifecycle system: events (SessionStart to SessionEnd), types (command/http/prompt/agent), exit codes, PreToolUse tool modification/denial. Customizes agent behavior.
npx claudepluginhub markus41/claude --plugin claude-code-expertHow this skill is triggered — by the user, by Claude, or both
Slash command
/claude-code-expert:hooks-systemThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Complete reference for the hooks lifecycle system.
Develops Claude Code plugin hooks for event-driven automation, validating tool use with prompt-based, command, and agent types for events like PreToolUse, Stop, and SessionStart.
Guides development of event-driven hooks for Claude Code plugins using prompt-based and command-based configurations in hooks.json for events like PreToolUse, PostToolUse, Stop, and SessionStart to validate tools and automate workflows.
Guides creating, configuring, and debugging Claude Code hooks for event-driven automation, command validation, workflow customization, and notifications on events like PreToolUse and UserPromptSubmit.
Share bugs, ideas, or general feedback.
Complete reference for the hooks lifecycle system.
Hooks are user-defined shell commands that execute at specific points in the Claude Code lifecycle. They run synchronously and can modify Claude's behavior by returning structured JSON.
| Event | When | Can Block? |
|---|---|---|
SessionStart | Session begins/resumes | No |
UserPromptSubmit | Before processing user prompt | Yes (exit 2) |
PreToolUse | Before tool execution | Yes (exit 2) |
PostToolUse | After tool succeeds | No |
PostToolUseFailure | After tool fails | No |
PermissionRequest | Permission prompt appears | No |
Notification | User needs attention | No |
SubagentStart | Sub-agent starts | No |
SubagentStop | Sub-agent finishes | Yes (continue) |
TaskCompleted | Task marked complete | No |
ConfigChange | Config file changed | No |
TeammateIdle | Agent team teammate going idle | No |
PreCompact | Before conversation history compacted | No |
InstructionsLoaded | After CLAUDE.md/rules loaded | No |
WorktreeCreate | Git worktree created for agent | No |
WorktreeRemove | Git worktree cleaned up | No |
SessionEnd | Session terminates | No |
Stop | Claude about to stop | Yes (continue) |
| Type | Description |
|---|---|
command | Shell script execution |
http | POST to a URL endpoint |
prompt | Single-turn LLM evaluation |
agent | Multi-turn verification with tools |
| Exit Code | Meaning |
|---|---|
0 | Proceed (approve). For UserPromptSubmit/SessionStart, stdout added to context |
2 | Block action (deny). stderr becomes feedback to Claude |
| Other | Log only (hook failure, tool proceeds normally) |
Fires before a tool is executed. Can approve, deny, or modify the tool call.
Trigger: Before each tool invocation Input (stdin): JSON with tool name and input parameters Output (stdout): JSON with decision
// Input received on stdin
{
"tool_name": "Bash",
"tool_input": {
"command": "rm -rf /tmp/data",
"description": "Delete temporary data"
},
"session_id": "abc123"
}
Response options:
// Approve (proceed normally)
{ "decision": "approve" }
// Deny (block the tool call)
{ "decision": "deny", "reason": "Destructive command blocked" }
// Modify (change the tool input)
{
"decision": "approve",
"tool_input": {
"command": "rm -rf /tmp/data --interactive",
"description": "Delete temporary data (with confirmation)"
}
}
// No output = approve (passthrough)
Fires after a tool completes. Can observe results or modify output.
Trigger: After each tool invocation Input (stdin): JSON with tool name, input, and output
{
"tool_name": "Bash",
"tool_input": {
"command": "npm test"
},
"tool_output": {
"stdout": "All tests passed",
"stderr": "",
"exitCode": 0
},
"session_id": "abc123"
}
Response: Optional JSON to modify what Claude sees.
Fires when Claude wants to notify the user (e.g., task completion).
Trigger: Claude sends a notification Input (stdin): JSON with notification details
{
"message": "Task completed successfully",
"type": "info",
"session_id": "abc123"
}
Use cases: Desktop notifications, Slack messages, sound alerts.
Fires when Claude is about to stop (end of conversation turn).
Trigger: Claude reaches a stopping point Input (stdin): JSON with session context
{
"session_id": "abc123",
"reason": "end_turn",
"message": "I've completed the task."
}
Response: Can force Claude to continue.
// Force continue with additional instructions
{
"decision": "continue",
"message": "Also run the linter before finishing."
}
// Allow stop (default)
{ "decision": "stop" }
Fires when a sub-agent (spawned via Agent tool) is about to stop.
Same schema as Stop but for sub-agents.
Hooks are configured in settings.json (project or user level):
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/pre-bash.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "python3 .claude/hooks/log-tool-use.py"
}
]
}
],
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/notify.sh"
}
]
}
],
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/on-stop.sh"
}
]
}
]
}
}
| Pattern | Matches |
|---|---|
"Bash" | Only Bash tool calls |
"Read" | Only Read tool calls |
"Write" | Only Write tool calls |
"Edit" | Only Edit tool calls |
"mcp__*" | All MCP tool calls |
"mcp__filesystem__*" | Specific MCP server tools |
"*" | All tool calls |
"" | Default/all (for non-tool hooks) |
#!/bin/bash
# .claude/hooks/security-guard.sh
# Block dangerous bash commands
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
if [ "$TOOL" = "Bash" ]; then
# Block rm -rf on important paths
if echo "$COMMAND" | grep -qE 'rm\s+-rf\s+(/|/home|/etc|/var)'; then
echo '{"decision": "deny", "reason": "Blocked: destructive rm -rf on system path"}'
exit 0
fi
# Block curl piped to bash
if echo "$COMMAND" | grep -qE 'curl.*\|\s*(ba)?sh'; then
echo '{"decision": "deny", "reason": "Blocked: piping curl to shell"}'
exit 0
fi
# Block sudo
if echo "$COMMAND" | grep -qE '^\s*sudo\s'; then
echo '{"decision": "deny", "reason": "Blocked: sudo commands"}'
exit 0
fi
fi
# Approve everything else
echo '{"decision": "approve"}'
#!/bin/bash
# .claude/hooks/log-tool-use.sh
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
echo "$TIMESTAMP | $TOOL" >> .claude/tool-usage.log
#!/bin/bash
# .claude/hooks/notify.sh
INPUT=$(cat)
MESSAGE=$(echo "$INPUT" | jq -r '.message')
# macOS
if command -v osascript &>/dev/null; then
osascript -e "display notification \"$MESSAGE\" with title \"Claude Code\""
fi
# Linux
if command -v notify-send &>/dev/null; then
notify-send "Claude Code" "$MESSAGE"
fi
#!/bin/bash
# .claude/hooks/auto-test.sh
INPUT=$(cat)
# Check if any source files were modified
if git diff --name-only | grep -qE '\.(ts|tsx|js|jsx)$'; then
# Run tests
if ! npm test --silent 2>/dev/null; then
echo '{"decision": "continue", "message": "Tests are failing. Please fix them before stopping."}'
exit 0
fi
fi
echo '{"decision": "stop"}'
#!/bin/bash
# .claude/hooks/lessons-learned-capture.sh
# Capture tool failures to lessons-learned.md
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
EXIT_CODE=$(echo "$INPUT" | jq -r '.tool_output.exitCode // .tool_output.exit_code // "0"')
ERROR=$(echo "$INPUT" | jq -r '.tool_output.stderr // .error // ""')
if [ "$EXIT_CODE" != "0" ] && [ -n "$ERROR" ]; then
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
TOOL_INPUT=$(echo "$INPUT" | jq -r '.tool_input | tostring' | head -c 200)
cat >> .claude/rules/lessons-learned.md << EOF
### Error: ${TOOL} failure (${TIMESTAMP})
- **Tool:** ${TOOL}
- **Input:** \`${TOOL_INPUT}\`
- **Error:** ${ERROR}
- **Status:** NEEDS_FIX
EOF
fi