Hook Development
Hook Development
Section titled “Hook Development”Learn how to create custom hooks that respond to lifecycle events in Claude Code.
Hook Basics
Section titled “Hook Basics”Hooks are event-driven scripts that execute at specific points in the Claude Code lifecycle.
Key Characteristics:
- Event-triggered execution
- JSON stdin/stdout protocol
- Cross-platform compatible
- Tested and validated
Hook Events
Section titled “Hook Events”Session Lifecycle
Section titled “Session Lifecycle”SessionStart: Session beginsSessionEnd: Session endsSessionResume: Session restored
Message Events
Section titled “Message Events”FirstMessage: Before first user messageUserMessage: After each user messageAssistantMessage: After Claude responds
File Events
Section titled “File Events”FileCreate: File createdFileModify: File modifiedFileDelete: File deleted
Git Events
Section titled “Git Events”GitCommit: Before/after commitsGitPush: Before/after pushGitPR: Pull request created
Hook Protocol
Section titled “Hook Protocol”Input Format (stdin)
Section titled “Input Format (stdin)”{ "event": "SessionStart", "timestamp": "2026-01-14T12:00:00Z", "data": { "cwd": "/path/to/project", "user": "username" }}Output Format (stdout)
Section titled “Output Format (stdout)”{ "status": "success", "message": "Hook executed successfully", "data": { "result": "value" }}Error Format (stdout)
Section titled “Error Format (stdout)”{ "status": "error", "message": "Error description", "error": { "code": "ERROR_CODE", "details": "Additional details" }}Creating Hooks
Section titled “Creating Hooks”1. Create Hook Script
Section titled “1. Create Hook Script”Create Python script in hooks/ directory:
#!/usr/bin/env python3"""SessionStart hook for my plugin"""import sysimport json
def main(): # Read input from stdin input_data = json.loads(sys.stdin.read())
# Extract event data event = input_data.get("event") data = input_data.get("data", {})
# Process event result = process_session_start(data)
# Output result to stdout output = { "status": "success", "message": "Session started successfully", "data": result } print(json.dumps(output))
def process_session_start(data): """Process session start event""" cwd = data.get("cwd") # Do something with the data return {"project": cwd}
if __name__ == "__main__": main()2. Register Hook
Section titled “2. Register Hook”Add to hooks/hooks.json:
{ "SessionStart": { "type": "command", "command": "python \"${CLAUDE_PLUGIN_ROOT}/hooks/session-start.py\"", "timeout": 10000 }}3. Test Hook
Section titled “3. Test Hook”Create test file in hooks/tests/:
import jsonimport subprocess
def test_session_start(): """Test SessionStart hook""" input_data = { "event": "SessionStart", "data": { "cwd": "/test/project" } }
# Run hook result = subprocess.run( ["python", "hooks/session-start.py"], input=json.dumps(input_data), capture_output=True, text=True )
# Verify output output = json.loads(result.stdout) assert output["status"] == "success" assert "project" in output["data"]Portability Standards
Section titled “Portability Standards”Path Conventions
Section titled “Path Conventions”Use ${CLAUDE_PLUGIN_ROOT} for plugin-relative paths:
{ "command": "python \"${CLAUDE_PLUGIN_ROOT}/hooks/my-hook.py\""}Windows Compatibility
Section titled “Windows Compatibility”- Double-quote paths
- Use forward slashes
- Handle drive letters
{ "command": "python \"${CLAUDE_PLUGIN_ROOT}/hooks/my-hook.py\""}Python Shebang
Section titled “Python Shebang”Use portable shebang:
#!/usr/bin/env python3Hook Configuration
Section titled “Hook Configuration”Timeout
Section titled “Timeout”Set reasonable timeout (milliseconds):
{ "SessionStart": { "type": "command", "command": "...", "timeout": 10000 // 10 seconds }}Error Handling
Section titled “Error Handling”Always handle errors gracefully:
def main(): try: input_data = json.loads(sys.stdin.read()) result = process_event(input_data) output = { "status": "success", "data": result } except Exception as e: output = { "status": "error", "message": str(e), "error": { "code": "HOOK_ERROR", "details": traceback.format_exc() } } print(json.dumps(output))Advanced Hooks
Section titled “Advanced Hooks”Async Operations
Section titled “Async Operations”For long-running operations:
import asyncio
async def process_async(data): # Long operation result = await fetch_data() return result
def main(): input_data = json.loads(sys.stdin.read()) result = asyncio.run(process_async(input_data["data"])) output = {"status": "success", "data": result} print(json.dumps(output))File Operations
Section titled “File Operations”Read/write files safely:
import osfrom pathlib import Path
def main(): input_data = json.loads(sys.stdin.read()) cwd = input_data["data"]["cwd"]
# Safe file path project_file = Path(cwd) / ".popkit" / "state.json"
# Ensure directory exists project_file.parent.mkdir(parents=True, exist_ok=True)
# Write data with open(project_file, "w") as f: json.dump({"state": "ready"}, f)External Commands
Section titled “External Commands”Run external commands:
import subprocess
def main(): input_data = json.loads(sys.stdin.read())
# Run git command result = subprocess.run( ["git", "status", "--porcelain"], cwd=input_data["data"]["cwd"], capture_output=True, text=True )
output = { "status": "success", "data": {"git_status": result.stdout} } print(json.dumps(output))Best Practices
Section titled “Best Practices”- Fast Execution: Keep hooks under 10 seconds
- Error Handling: Always handle and report errors
- JSON Protocol: Strict adherence to protocol
- Portability: Use standards for cross-platform
- Testing: Comprehensive test coverage
- Documentation: Clear purpose and behavior
- Logging: Log to file, not stdout
Example: Custom Git Hook
Section titled “Example: Custom Git Hook”#!/usr/bin/env python3"""GitCommit hook for custom validation"""import sysimport jsonimport subprocessfrom pathlib import Path
def main(): try: input_data = json.loads(sys.stdin.read()) data = input_data.get("data", {})
# Validate commit validation = validate_commit(data)
if not validation["valid"]: output = { "status": "error", "message": validation["message"] } else: output = { "status": "success", "message": "Commit validation passed", "data": validation }
print(json.dumps(output))
except Exception as e: output = { "status": "error", "message": str(e) } print(json.dumps(output))
def validate_commit(data): """Validate commit meets requirements""" cwd = data.get("cwd")
# Check for required patterns result = subprocess.run( ["git", "diff", "--staged"], cwd=cwd, capture_output=True, text=True )
diff = result.stdout
# Custom validation logic if "TODO" in diff: return { "valid": False, "message": "Commit contains TODO comments" }
return { "valid": True, "message": "Commit validation passed" }
if __name__ == "__main__": main()Troubleshooting
Section titled “Troubleshooting”Hook Not Executing
Section titled “Hook Not Executing”Symptom: Hook doesn’t run
Solution:
- Check
hooks.jsonsyntax - Verify script permissions
- Test script manually
- Check timeout value
JSON Parse Errors
Section titled “JSON Parse Errors”Symptom: Hook fails with parse error
Solution:
- Validate JSON output
- Check for extra stdout
- Remove debug prints
- Use proper JSON encoding
Timeout Errors
Section titled “Timeout Errors”Symptom: Hook times out
Solution:
- Optimize hook execution
- Increase timeout value
- Move slow operations to background
- Use async for long operations
Next Steps
Section titled “Next Steps”- Review Custom Skills
- Learn about Agent Configuration
- Explore existing hooks in PopKit packages