Hooks are event-driven automation scripts that execute in response to Claude Code events. Use hooks to validate operations, enforce policies, add context, and integrate external tools into workflows. **Key capabilities:** - Validate tool calls before execution (PreToolUse)
{ "type": "prompt", "prompt": "Evaluate if this tool use is appropriate: $TOOL_INPUT", "timeout": 30 }
{ "type": "command", "command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh", "timeout": 60 }
hooks/hooks.json, use wrapper format:{ "description": "Brief explanation of hooks (optional)", "hooks": { "PreToolUse": [...], "Stop": [...], "SessionStart": [...] } }
description field is optionalhooks field is required wrapper containing actual hook events{ "description": "Validation hooks for code quality", "hooks": { "PreToolUse": [ { "matcher": "Write", "hooks": [ { "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/validate.sh" } ] } ] } }
.claude/settings.json, use direct format:{ "PreToolUse": [...], "Stop": [...], "SessionStart": [...] }
{"hooks": {...}}.{ "PreToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "prompt", "prompt": "Validate file write safety. Check: system paths, credentials, path traversal, sensitive content. Return 'approve' or 'deny'." } ] } ] } `**Output for PreToolUse:**` { "hookSpecificOutput": { "permissionDecision": "allow|deny|ask", "updatedInput": {"field": "modified_value"} }, "systemMessage": "Explanation for Claude" }
{ "PostToolUse": [ { "matcher": "Edit", "hooks": [ { "type": "prompt", "prompt": "Analyze edit result for potential issues: syntax errors, security vulnerabilities, breaking changes. Provide feedback." } ] } ] }
{ "Stop": [ { "matcher": "*", "hooks": [ { "type": "prompt", "prompt": "Verify task completion: tests run, build succeeded, questions answered. Return 'approve' to stop or 'block' with reason to continue." } ] } ] } `**Decision output:**` { "decision": "approve|block", "reason": "Explanation", "systemMessage": "Additional context" }
{ "UserPromptSubmit": [ { "matcher": "*", "hooks": [ { "type": "prompt", "prompt": "Check if prompt requires security guidance. If discussing auth, permissions, or API security, return relevant warnings." } ] } ] }
{ "SessionStart": [ { "matcher": "*", "hooks": [ { "type": "command", "command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/load-context.sh" } ] } ] } `**Special capability:** Persist environment variables using `$CLAUDE_ENV_FILE`:` echo "export PROJECT_TYPE=nodejs" >> "$CLAUDE_ENV_FILE"
examples/load-context.sh for complete example.{ "continue": true, "suppressOutput": false, "systemMessage": "Message for Claude" }
continue: If false, halt processing (default true)suppressOutput: Hide output from transcript (default false)systemMessage: Message shown to Claude0 - Success (stdout shown in transcript)2 - Blocking error (stderr fed back to Claude){ "session_id": "abc123", "transcript_path": "/path/to/transcript.txt", "cwd": "/current/working/dir", "permission_mode": "ask|allow", "hook_event_name": "PreToolUse" }
tool_name, tool_input, tool_resultuser_promptreason$TOOL_INPUT, $TOOL_RESULT, $USER_PROMPT, etc.$CLAUDE_PROJECT_DIR - Project root path$CLAUDE_PLUGIN_ROOT - Plugin directory (use for portable paths)$CLAUDE_ENV_FILE - SessionStart only: persist env vars here$CLAUDE_CODE_REMOTE - Set if running in remote context{ "type": "command", "command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh" }
hooks/hooks.json:{ "PreToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "prompt", "prompt": "Validate file write safety" } ] } ], "Stop": [ { "matcher": "*", "hooks": [ { "type": "prompt", "prompt": "Verify task completion" } ] } ], "SessionStart": [ { "matcher": "*", "hooks": [ { "type": "command", "command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/load-context.sh", "timeout": 10 } ] } ] }
"matcher": "Write""matcher": "Read|Write|Edit""matcher": "*""matcher": "mcp__.*__delete.*" // All MCP delete tools// All MCP tools "matcher": "mcp__.*" // Specific plugin's MCP tools "matcher": "mcp__plugin_asana_.*" // All file operations "matcher": "Read|Write|Edit" // Bash commands only "matcher": "Bash"
#!/bin/bash set -euo pipefail input=$(cat) tool_name=$(echo "$input" | jq -r '.tool_name') # Validate tool name format if [[ ! "$tool_name" =~ ^[a-zA-Z0-9_]+$ ]]; then echo '{"decision": "deny", "reason": "Invalid tool name"}' >&2 exit 2 fi
file_path=$(echo "$input" | jq -r '.tool_input.file_path') # Deny path traversal if [[ "$file_path" == *".."* ]]; then echo '{"decision": "deny", "reason": "Path traversal detected"}' >&2 exit 2 fi # Deny sensitive files if [[ "$file_path" == *".env"* ]]; then echo '{"decision": "deny", "reason": "Sensitive file"}' >&2 exit 2 fi
examples/validate-write.sh and examples/validate-bash.sh for complete examples.# GOOD: Quoted echo "$file_path" cd "$CLAUDE_PROJECT_DIR" # BAD: Unquoted (injection risk) echo $file_path cd $CLAUDE_PROJECT_DIR `### Set Appropriate Timeouts` { "type": "command", "command": "bash script.sh", "timeout": 10 }
{ "PreToolUse": [ { "matcher": "Write", "hooks": [ {"type": "command", "command": "check1.sh"}, // Parallel {"type": "command", "command": "check2.sh"}, // Parallel {"type": "prompt", "prompt": "Validate..."} // Parallel ] } ] }
#!/bin/bash # Only active when flag file exists FLAG_FILE="$CLAUDE_PROJECT_DIR/.enable-strict-validation" if [ ! -f "$FLAG_FILE" ]; then # Flag not present, skip validation exit 0 fi # Flag present, run validation input=$(cat) # ... validation logic ... `**Pattern: Configuration-based activation**` #!/bin/bash # Check configuration for activation CONFIG_FILE="$CLAUDE_PROJECT_DIR/.claude/plugin-config.json" if [ -f "$CONFIG_FILE" ]; then enabled=$(jq -r '.strictMode // false' "$CONFIG_FILE") if [ "$enabled" != "true" ]; then exit 0 # Not enabled, skip fi fi # Enabled, run hook logic input=$(cat) # ... hook logic ...
hooks/hooks.json won't affect current sessionclaude againclaude or ccclaude --debug/hooks command to review loaded hooks in current session.claude --debugecho '{"tool_name": "Write", "tool_input": {"file_path": "/test"}}' | \ bash ${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh echo "Exit code: $?"
output=$(./your-hook.sh < test-input.json) echo "$output" | jq .
references/patterns.md - Common hook patterns (8+ proven patterns)references/migration.md - Migrating from basic to advanced hooksreferences/advanced.md - Advanced use cases and techniquesexamples/:validate-write.sh - File write validation examplevalidate-bash.sh - Bash command validation exampleload-context.sh - SessionStart context loading examplescripts/:validate-hook-schema.sh - Validate hooks.json structure and syntaxtest-hook.sh - Test hooks with sample input before deploymenthook-linter.sh - Check hook scripts for common issues and best practicesclaude --debug for detailed logsjq to validate hook JSON outputhooks/hooks.jsonscripts/validate-hook-schema.sh hooks/hooks.jsonscripts/test-hook.sh before deploymentclaude --debug