Claude Code Hooks System
SkillAI & modelsA skill for ai & models by thelobbi.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Claude Code Hooks System skill
What this skill tells your AI
The instructions your AI receives, as published by thelobbi/claude in plugins/claude-code-expert/skills-old/hooks-system/SKILL.md and read by ahel’s review.
Complete reference for the hooks lifecycle system.
Overview
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.
Hook Types
All Lifecycle Events
| 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) |
Hook Types (4 types)
| Type | Description |
|---|---|
command | Shell script execution |
http | POST to a URL endpoint |
prompt | Single-turn LLM evaluation |
agent | Multi-turn verification with tools |
Hook Exit Codes
| 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) |
PreToolUse
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)
PostToolUse
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.
Notification
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.
Stop
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" }
SubagentStop
Fires when a sub-agent (spawned via Agent tool) is about to stop.
Same schema as Stop but for sub-agents.
Configuration
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"
}
]
}
]
}
}
Matcher Patterns
| 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) |
Example Hook Scripts
Security Guard (PreToolUse)
#!/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"}'
Tool Usage Logger (PostToolUse)
#!/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
Desktop Notification (Notification)
#!/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
Auto-Test on Stop (Stop)
#!/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"}'
Error Capture (PostToolUse)
#!/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
Hook Execution Details
- Hooks run synchronously — Claude waits for them to complete
- Hooks receive input on stdin as JSON
- Hooks output responses on stdout as JSON
- Non-zero exit codes are treated as hook failures (tool proceeds as normal)
- Hooks have a timeout (default varies by hook type)
- Hooks run in the project root directory
- Hooks have access to the full environment (env vars, PATH, etc.)
- Multiple hooks can be chained for the same matcher — they run in order
Best Practices
- Keep hooks fast — They block Claude's execution
- Use jq for JSON parsing — It's reliable and available everywhere
- Handle errors gracefully — A crashing hook shouldn't block Claude
- Log hook activity — For debugging hook behavior
- Test hooks independently — Run them with sample JSON input
- Use matchers narrowly — Don't run expensive hooks on every tool call
- Return valid JSON — Invalid JSON is ignored (treated as approve)
Signals
- GitHub stars
- 21
- Forks
- 2
- Last commit
- Sep 2026
ahel review
K1binfo
installs-packages (in scripts/validate-hooks.sh)
Automated review, not a security audit. Ruleset v1+k2.
Advanced
- Catalog kind
- skill
- Gateway key
hooks-system- Source
- github.com/thelobbi/claude