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.
What Are Hooks?
Hooks allow you to:
Validate tool calls before execution (PreToolUse)
React to tool results (PostToolUse)
Enforce completion standards (Stop, SubagentStop)
Load project context (SessionStart)
Automate workflows across the development lifecycle
Hook Types
Prompt-Based Hooks (Recommended)
Use LLM-driven decision making for context-aware validation:
{
"type" : "prompt" ,
"prompt" : "Evaluate if this file write is safe. Check for: system paths, credentials, sensitive content. Return 'approve' or 'deny'." ,
"timeout" : 30
}
Supported events : Stop, SubagentStop, UserPromptSubmit, PreToolUse
Benefits:
Context-aware decisions based on natural language reasoning
Flexible evaluation logic without bash scripting
Better edge case handling
Easier to maintain and extend
Command Hooks
Execute bash commands for deterministic checks:
{
"type" : "command" ,
"command" : "bash ${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh" ,
"timeout" : 60
}
Use for:
Fast deterministic validations
File system operations
External tool integrations
Performance-critical checks
For plugin hooks in hooks/hooks.json:
{
"description" : "Validation hooks for code quality" ,
"hooks" : {
"PreToolUse" : [
{
"matcher" : "Write" ,
"hooks" : [
{
"type" : "command" ,
"command" : "${CLAUDE_PLUGIN_ROOT}/hooks/validate.sh"
}
]
}
]
}
}
The hooks wrapper is required for plugin hooks.json files.
For user settings in .claude/settings.json, use direct format (no wrapper):
{
"PreToolUse" : [
{
"matcher" : "Write|Edit" ,
"hooks" : [
{
"type" : "prompt" ,
"prompt" : "Validate file safety..."
}
]
}
]
}
Hook Events
Execute before any tool runs. Use to approve, deny, or modify tool calls.
Example (prompt-based):
{
"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:
{
"hookSpecificOutput" : {
"permissionDecision" : "allow|deny|ask" ,
"updatedInput" : { "field" : "modified_value" }
},
"message" : "Optional message to user"
}
Example (command-based):
{
"PreToolUse" : [
{
"matcher" : "Bash" ,
"hooks" : [
{
"type" : "command" ,
"command" : "bash ${CLAUDE_PLUGIN_ROOT}/hooks/validate-bash.sh" ,
"timeout" : 30
}
]
}
]
}
PostToolUse
Execute after a tool completes. Use to process results or trigger follow-up actions.
Example:
{
"PostToolUse" : [
{
"matcher" : "Write|Edit" ,
"hooks" : [
{
"type" : "command" ,
"command" : "bash ${CLAUDE_PLUGIN_ROOT}/hooks/format-file.sh"
}
]
}
]
}
Output:
{
"hookSpecificOutput" : {
"updatedOutput" : { "field" : "modified_value" }
},
"message" : "File formatted successfully"
}
Stop
Execute when Claude wants to end the session. Use to enforce completion criteria.
Example (prompt-based):
{
"Stop" : [
{
"hooks" : [
{
"type" : "prompt" ,
"prompt" : "Check if all requirements are met before stopping: tests run, code reviewed, documentation updated. Return 'approve' to allow stop or 'deny' with explanation."
}
]
}
]
}
Output:
{
"hookSpecificOutput" : {
"stopDecision" : "allow|deny"
},
"message" : "Please run tests before completing"
}
SessionStart
Execute when Claude Code session begins. Use to load context or configure environment.
Example:
{
"SessionStart" : [
{
"hooks" : [
{
"type" : "command" ,
"command" : "bash ${CLAUDE_PLUGIN_ROOT}/hooks/load-context.sh"
}
]
}
]
}
Output:
{
"message" : "Project context loaded: React 18, TypeScript 5.0"
}
UserPromptSubmit
Execute when user submits a prompt. Use to validate or enrich user input.
Example:
{
"UserPromptSubmit" : [
{
"hooks" : [
{
"type" : "prompt" ,
"prompt" : "Check if user request contains sensitive data or inappropriate content. Return 'approve' or 'deny'."
}
]
}
]
}
SubagentStop
Execute when a subagent wants to complete. Similar to Stop but for agents.
PreCompact
Execute before context compaction. Use to preserve critical information.
Notification
Execute on system notifications.
Matchers
Filter which tool calls trigger your hook:
{
"matcher" : "Write|Edit" ,
"hooks" : [ ... ]
}
Common patterns:
"Write" - Only Write tool
"Write|Edit" - Write or Edit tools
"Bash" - Only Bash tool
".*" - All tools (use cautiously)
No Matcher
For events without tool context (Stop, SessionStart, etc.), omit matcher:
{
"SessionStart" : [
{
"hooks" : [ ... ]
}
]
}
Environment Variables
Use for portable path references:
{
"command" : "bash ${CLAUDE_PLUGIN_ROOT}/hooks/validate.sh"
}
Always use $ for paths. Never use hardcoded absolute paths.
Custom Variables
Access environment variables in hooks:
{
"command" : "bash ${CLAUDE_PLUGIN_ROOT}/hooks/deploy.sh" ,
"env" : {
"DEPLOY_ENV" : "${DEPLOY_ENV}" ,
"API_KEY" : "${API_KEY}"
}
}
Hooks receive JSON input via stdin:
PreToolUse:
{
"toolName" : "Write" ,
"toolInput" : {
"file_path" : "/path/to/file.ts" ,
"content" : "file contents"
}
}
Stop:
{
"transcript" : "..." ,
"sessionContext" : { ... }
}
Hook Output
Return JSON to stdout:
Allow:
{
"hookSpecificOutput" : {
"permissionDecision" : "allow"
},
"message" : "Operation approved"
}
Deny:
{
"hookSpecificOutput" : {
"permissionDecision" : "deny"
},
"message" : "Cannot write to system directory"
}
Modify:
{
"hookSpecificOutput" : {
"permissionDecision" : "allow" ,
"updatedInput" : {
"file_path" : "/safe/path/file.ts"
}
}
}
Example Hooks
Validate Dangerous Commands
hooks/scripts/validate-bash.sh:
#!/bin/bash
set -e
input = $( cat )
command = $( echo " $input " | jq -r '.toolInput.command' )
# Check for dangerous patterns
if echo " $command " | grep -qE "rm\s+-rf|dd\s+if=|mkfs" ; then
echo '{"hookSpecificOutput":{"permissionDecision":"deny"},"message":"Dangerous command blocked"}'
exit 0
fi
echo '{"hookSpecificOutput":{"permissionDecision":"allow"}}'
Load Project Context
hooks/scripts/load-context.sh:
#!/bin/bash
set -e
context = ""
# Add package.json info if exists
if [ -f package.json ]; then
framework = $( jq -r '.dependencies | keys[] | select(. == "react" or . == "vue" or . == "angular")' package.json | head -1 )
if [ -n " $framework " ]; then
context = "Using $framework framework. "
fi
fi
echo "{ \" message \" : \" $context Project context loaded \" }"
Enforce Test Running
hooks/hooks.json:
{
"hooks" : {
"Stop" : [
{
"hooks" : [
{
"type" : "prompt" ,
"prompt" : "Check the transcript to see if tests were run. If no test command appears (npm test, pytest, cargo test, etc.), deny with message to run tests first. Otherwise approve."
}
]
}
]
}
}
Best Practices
Use Prompt Hooks Prefer prompt-based hooks for flexible, context-aware logic
Portable Paths Always use $ for script paths
Set Timeouts Add reasonable timeouts to prevent hanging (default 120s)
Validate Input Always validate hook input before processing
Security Considerations
Hooks execute with full system access. Validate all inputs and sanitize data before use.
Input validation : Always validate and sanitize hook inputs
Path safety : Check for path traversal attempts
Command injection : Never use unvalidated input in shell commands
Least privilege : Run hooks with minimal necessary permissions
Timeout protection : Set reasonable timeouts to prevent DoS
Testing Hooks
Manual Testing
# Test with sample input
echo '{"toolName":"Write","toolInput":{"file_path":"/tmp/test.txt"}}' | \
bash hooks/scripts/validate.sh
Validation Script
Use plugin-dev toolkit’s validation utilities:
# Validate hooks.json schema
./validate-hook-schema.sh hooks/hooks.json
# Test hook script
./test-hook.sh hooks/scripts/validate.sh test-input.json
Troubleshooting
Hook Not Triggering
Check matcher pattern matches tool name
Verify hooks.json syntax
Check hook is in correct event (PreToolUse vs PostToolUse)
Enable debug mode: claude --debug
Hook Fails Silently
Check script has execute permissions: chmod +x script.sh
Verify $ resolves correctly
Check script outputs valid JSON
Review timeout settings
Permission Decision Ignored
Ensure permissionDecision is in hookSpecificOutput
Check spelling: “allow” not “approve”
Verify JSON format is correct
Check hook type supports decision (PreToolUse, Stop)
Next Steps
MCP Integration Connect external services and APIs
Skills Create auto-activating expertise