HOW-TO: Create a New Hook
Estimated Time: 30-50 minutes (simple hook: 30 min, complex with validation: 50 min) Difficulty: Intermediate Prerequisites: Shell scripting or Python knowledge, understanding of CODITECT framework
Overview
This guide walks you through creating a new hook for the CODITECT framework from scratch. By the end, you'll have a production-ready hook that automatically triggers at specific lifecycle events.
What You'll Learn:
- How to choose the right hook event type
- How to structure hook scripts with proper I/O handling
- How to implement exit code conventions for blocking/non-blocking behavior
- How to configure hooks in settings.json
- How to test and validate your hook
What You'll Build:
A working pre-tool-component-validation.sh hook that validates CODITECT component files before they're modified, ensuring YAML frontmatter compliance and preventing common errors.
Prerequisites
Before you begin, ensure you have:
- CODITECT framework installed and configured
- Access to
.claude/hooks/directory - Shell scripting (bash/zsh) OR Python 3.10+ knowledge
- Understanding of JSON format
-
jqinstalled (for JSON parsing in shell scripts) - Read CODITECT-STANDARD-HOOKS.md (recommended)
Knowledge Requirements:
- Hook events (CRITICAL) - Understand 10 event types and when to use each
- Exit codes (CRITICAL) - 0=success, 2=blocking, 1=warning
- JSON I/O - Hooks read stdin (JSON) and write stdout (JSON)
- Performance - Hooks must complete within 5 seconds (30s hard timeout)
Step 1: Choose Hook Event Type
Time: 5 minutes
Select the appropriate event type based on when you want the hook to trigger and whether it needs blocking capability.
Event Selection Decision Tree
What do you need to do?
├─ Validate BEFORE action happens?
│ ├─ Tool execution? → PreToolUse (can block)
│ ├─ User prompt? → UserPromptSubmit (can block)
│ └─ Operation stop? → Stop (can block)
│
├─ React AFTER action completes?
│ ├─ Tool finished? → PostToolUse (cannot block)
│ ├─ Permission granted? → PermissionRequest (cannot block)
│ └─ Notification sent? → Notification (cannot block)
│
└─ Session lifecycle?
├─ Session starts? → SessionStart (cannot block)
├─ Session ends? → SessionEnd (cannot block)
└─ Context compaction? → PreCompact (cannot block)
Event Capabilities
| Event | Can Block | Typical Duration | Best For |
|---|---|---|---|
| PreToolUse | ✅ Yes | <2s | Validation, security checks |
| PostToolUse | ❌ No | <3s | Formatting, cleanup |
| UserPromptSubmit | ✅ Yes | <1s | Prompt enhancement |
| SessionStart | ❌ No | <5s | Initialization, context loading |
| SessionEnd | ❌ No | <5s | Cleanup, state saving |
Our Example
Need: Validate component files before modification
Event: PreToolUse (can block invalid operations)
Why: Need to prevent edits to components without YAML frontmatter
Blocking: Yes (exit 2 if validation fails)
Step 2: Define Hook Purpose and Scope
Time: 5 minutes
Clearly define what your hook does and its boundaries.
Questions to Answer
-
What problem does this solve?
- Example: "Prevent corruption of CODITECT component files by enforcing YAML frontmatter"
-
Which tools trigger this hook?
- Write, Edit tools when targeting component files
-
What are the validation criteria?
- File must be in agents/, commands/, or skills/ directory
- File must have YAML frontmatter with
---delimiters - Required fields must be present (name, description)
-
What happens on success vs. failure?
- Success: Allow tool to proceed (exit 0)
- Failure: Block tool with helpful error message (exit 2)
-
What are the edge cases?
- New file creation (may not have frontmatter yet - allow)
- Non-component files (don't validate - allow)
- Files in subdirectories (check parent path)
Example: Component Validation Hook
Purpose: Enforce YAML frontmatter standards for CODITECT component files
Scope: agents/*.md, commands/*.md, skills/*/SKILL.md
Triggers: PreToolUse (Edit, Write tools)
Blocking: Yes (exit 2 on validation failure)
Performance: <500ms (file read + regex validation)
Document Your Specification
# Hook Specification: pre-tool-component-validation
## Purpose
Validate CODITECT component files have required YAML frontmatter before modification.
## Trigger
PreToolUse event (before Edit or Write tool execution)
## Logic
1. Check if target file is a component file (agents/, commands/, skills/)
2. If not a component file, allow (exit 0)
3. If file doesn't exist (new creation), allow (exit 0)
4. If file exists, check for YAML frontmatter
5. If frontmatter missing or invalid, block (exit 2)
6. If frontmatter valid, allow (exit 0)
## Validation Rules
- Must have opening `---` delimiter
- Must have closing `---` delimiter
- Must have `name:` field
- Must have `description:` field
## Error Messages
- "Component file missing YAML frontmatter"
- "Invalid YAML frontmatter structure"
- "Missing required field: {field_name}"
Step 3: Create Hook File
Time: 3 minutes
Create the hook file with proper naming and permissions.
Choose Language
Shell (bash/zsh):
- ✅ Faster startup
- ✅ Better for simple text processing
- ❌ More verbose JSON handling
Python:
- ✅ Easier JSON handling
- ✅ Better for complex logic
- ❌ Slower startup (~100ms overhead)
Create File
For shell hook:
# Navigate to project hooks directory
cd .claude/hooks/
# Create hook file
touch pre-tool-component-validation.sh
# Make executable
chmod +x pre-tool-component-validation.sh
For Python hook:
cd .claude/hooks/
touch pre-tool-component-validation.py
chmod +x pre-tool-component-validation.py
Naming Convention
Pattern: {event}-{purpose}.{ext}
| Component | Rule | Example |
|---|---|---|
| Event | Lowercase, hyphenated | pre-tool, post-tool, user-prompt |
| Purpose | Descriptive, 2-4 words | component-validation, format-output |
| Extension | .sh or .py | .sh (preferred for speed) |
Maximum length: 64 characters (without extension)
Step 4: Write Hook Shebang and Imports
Time: 2 minutes
Start with proper shebang and required dependencies.
Shell Script Template
#!/bin/bash
# Hook: pre-tool-component-validation
# Purpose: Validate CODITECT component files before modification
# Event: PreToolUse
# Author: CODITECT Core Team
# Version: 1.0.0
set -euo pipefail # Exit on error, undefined vars, pipe failures
# Required: jq for JSON parsing
if ! command -v jq &> /dev/null; then
echo '{"continue": false, "message": "Hook requires jq: brew install jq"}' >&2
exit 2
fi
Python Script Template
#!/usr/bin/env python3
"""
Hook: pre-tool-component-validation
Purpose: Validate CODITECT component files before modification
Event: PreToolUse
Author: CODITECT Core Team
Version: 1.0.0
"""
import json
import sys
import re
from pathlib import Path
from typing import Dict, Any, Tuple
Step 5: Read and Parse Hook Input
Time: 5 minutes
Hooks receive JSON input on stdin. Parse it carefully with error handling.
Shell: Read JSON Input
# Read stdin into variable
HOOK_INPUT=$(cat)
# Parse JSON fields using jq
TOOL_NAME=$(echo "$HOOK_INPUT" | jq -r '.tool_name // empty')
FILE_PATH=$(echo "$HOOK_INPUT" | jq -r '.tool_input.file_path // empty')
# Validate required fields
if [ -z "$TOOL_NAME" ] || [ -z "$FILE_PATH" ]; then
echo '{"continue": true, "message": "Hook: missing required input fields"}' >&2
exit 0 # Allow operation to proceed
fi
Python: Read JSON Input
def read_hook_input() -> Dict[str, Any]:
"""Read and parse JSON input from stdin."""
try:
hook_input = json.load(sys.stdin)
return hook_input
except json.JSONDecodeError as e:
# Invalid JSON - allow operation (fail open)
result = {
"continue": True,
"message": f"Hook: failed to parse JSON input: {e}"
}
print(json.dumps(result), file=sys.stderr)
sys.exit(0)
def extract_tool_info(hook_input: Dict[str, Any]) -> Tuple[str, str]:
"""Extract tool name and file path from hook input."""
tool_name = hook_input.get('tool_name', '')
file_path = hook_input.get('tool_input', {}).get('file_path', '')
return tool_name, file_path
Step 6: Implement Validation Logic
Time: 15 minutes
This is the core of your hook. Implement the validation rules defined in Step 2.
Shell: Validation Implementation
# Function: Check if file is a component file
is_component_file() {
local filepath="$1"
# Check if path contains component directories
if [[ "$filepath" == *"/agents/"*.md ]] || \
[[ "$filepath" == *"/commands/"*.md ]] || \
[[ "$filepath" == *"/skills/"*"/SKILL.md" ]]; then
return 0 # Is component file
fi
return 1 # Not component file
}
# Function: Validate YAML frontmatter
validate_frontmatter() {
local filepath="$1"
# Check if file exists
if [ ! -f "$filepath" ]; then
return 0 # New file - allow creation
fi
# Read file content
local content=$(cat "$filepath")
# Check for opening delimiter
if ! echo "$content" | grep -q "^---"; then
echo '{"continue": false, "message": "Component file missing YAML frontmatter opening delimiter (---)"}'
return 2
fi
# Check for closing delimiter
if ! echo "$content" | sed -n '2,$p' | grep -q "^---"; then
echo '{"continue": false, "message": "Component file missing YAML frontmatter closing delimiter (---)"}'
return 2
fi
# Extract frontmatter
local frontmatter=$(echo "$content" | sed -n '/^---$/,/^---$/p' | sed '1d;$d')
# Check for required fields
if ! echo "$frontmatter" | grep -q "^name:"; then
echo '{"continue": false, "message": "Component file missing required field: name"}'
return 2
fi
if ! echo "$frontmatter" | grep -q "^description:"; then
echo '{"continue": false, "message": "Component file missing required field: description"}'
return 2
fi
return 0
}
# Main validation logic
if is_component_file "$FILE_PATH"; then
if ! validate_frontmatter "$FILE_PATH"; then
exit 2 # Block operation
fi
fi
# Validation passed
echo '{"continue": true, "message": "Component validation passed"}'
exit 0
Python: Validation Implementation
def is_component_file(file_path: str) -> bool:
"""Check if file is a CODITECT component file."""
path = Path(file_path)
path_str = str(path)
# Check component directories
if '/agents/' in path_str and path.suffix == '.md':
return True
if '/commands/' in path_str and path.suffix == '.md':
return True
if '/skills/' in path_str and path.name == 'SKILL.md':
return True
return False
def validate_yaml_frontmatter(file_path: str) -> Tuple[bool, str]:
"""
Validate YAML frontmatter in component file.
Returns:
(is_valid, error_message)
"""
path = Path(file_path)
# New file - allow creation
if not path.exists():
return True, ""
# Read file content
try:
content = path.read_text(encoding='utf-8')
except Exception as e:
return False, f"Failed to read file: {e}"
# Check for frontmatter delimiters
if not content.startswith('---\n'):
return False, "Component file missing YAML frontmatter opening delimiter (---)"
# Find closing delimiter
parts = content.split('\n---\n', 2)
if len(parts) < 2:
return False, "Component file missing YAML frontmatter closing delimiter (---)"
# Extract frontmatter
frontmatter = parts[0].replace('---\n', '', 1)
# Check required fields
if not re.search(r'^name:\s*\S', frontmatter, re.MULTILINE):
return False, "Component file missing required field: name"
if not re.search(r'^description:\s*\S', frontmatter, re.MULTILINE):
return False, "Component file missing required field: description"
return True, ""
def main() -> int:
"""Main hook execution."""
# Read hook input
hook_input = read_hook_input()
tool_name, file_path = extract_tool_info(hook_input)
# Skip if no file path
if not file_path:
result = {"continue": True, "message": "Hook: no file path in input"}
print(json.dumps(result))
return 0
# Check if component file
if not is_component_file(file_path):
# Not a component file - allow
result = {"continue": True, "message": "Not a component file"}
print(json.dumps(result))
return 0
# Validate frontmatter
is_valid, error_message = validate_yaml_frontmatter(file_path)
if not is_valid:
# Validation failed - block operation
result = {"continue": False, "message": error_message}
print(json.dumps(result))
return 2
# Validation passed
result = {"continue": True, "message": "Component validation passed"}
print(json.dumps(result))
return 0
if __name__ == "__main__":
sys.exit(main())
Step 7: Implement Exit Code Handling
Time: 3 minutes
Proper exit codes are CRITICAL for hook behavior.
Exit Code Reference
| Exit Code | Meaning | Hook Behavior | Use When |
|---|---|---|---|
| 0 | Success | Allow operation to proceed | Validation passed or hook doesn't apply |
| 2 | Blocking error | BLOCK operation | Critical validation failure |
| 1 | Warning | Allow with warning logged | Non-critical issue detected |
Shell: Exit Code Pattern
# Success - allow operation
echo '{"continue": true, "message": "Validation passed"}'
exit 0
# Block operation - critical error
echo '{"continue": false, "message": "Validation failed: missing YAML frontmatter"}'
exit 2
# Warning - allow but log
echo '{"continue": true, "message": "Warning: non-standard formatting detected"}' >&2
exit 1
Python: Exit Code Pattern
# Success - allow operation
result = {"continue": True, "message": "Validation passed"}
print(json.dumps(result))
sys.exit(0)
# Block operation - critical error
result = {"continue": False, "message": "Validation failed: missing YAML frontmatter"}
print(json.dumps(result))
sys.exit(2)
# Warning - allow but log
result = {"continue": True, "message": "Warning: non-standard formatting"}
print(json.dumps(result), file=sys.stderr)
sys.exit(1)
Step 8: Add Error Handling
Time: 5 minutes
Robust error handling prevents hooks from breaking the development workflow.
Shell: Error Handling
#!/bin/bash
set -euo pipefail
# Trap errors
trap 'error_handler $? $LINENO' ERR
error_handler() {
local exit_code=$1
local line_number=$2
# Log error but allow operation (fail open)
echo "{\"continue\": true, \"message\": \"Hook error at line $line_number (code $exit_code)\"}" >&2
exit 0 # Allow operation despite hook error
}
# Alternative: Timeout protection
timeout 5s bash -c '
# Hook logic here
echo "Processing..."
' || {
echo '{"continue": true, "message": "Hook timeout - allowed operation"}' >&2
exit 0
}
Python: Error Handling
import sys
import json
from typing import NoReturn
def fail_open(message: str) -> NoReturn:
"""
Fail open - allow operation despite hook error.
Used when hook encounters unexpected errors.
"""
result = {
"continue": True,
"message": f"Hook error: {message}"
}
print(json.dumps(result), file=sys.stderr)
sys.exit(0)
def main() -> int:
try:
# Hook logic here
hook_input = read_hook_input()
# ... validation ...
except KeyboardInterrupt:
fail_open("Hook interrupted by user")
except TimeoutError:
fail_open("Hook timeout exceeded")
except Exception as e:
fail_open(f"Unexpected error: {e}")
return 0
Step 9: Configure Hook in settings.json
Time: 5 minutes
Register your hook in .claude/settings.json to activate it.
Configuration Format
{
"hooks": [
{
"event": "PreToolUse",
"script": ".claude/hooks/pre-tool-component-validation.sh",
"matcher": {
"tool_name": ["Edit", "Write"],
"file_pattern": "**/{agents,commands,skills}/**/*.md"
}
}
]
}
Configuration Fields
| Field | Type | Required | Description |
|---|---|---|---|
event | string | ✅ Yes | Hook event type (PreToolUse, PostToolUse, etc.) |
script | string | ✅ Yes | Path to hook script (relative to project root) |
matcher | object | ❌ No | Conditions for when hook runs |
matcher.tool_name | array | ❌ No | Tool names to match (Edit, Write, Read, etc.) |
matcher.file_pattern | string | ❌ No | Glob pattern for file paths |
Matcher Examples
Match specific tools:
"matcher": {
"tool_name": ["Edit", "Write"]
}
Match file patterns:
"matcher": {
"file_pattern": "**/*.md"
}
Match both:
"matcher": {
"tool_name": ["Edit"],
"file_pattern": "**/agents/*.md"
}
No matcher (runs on all events):
{
"event": "PreToolUse",
"script": ".claude/hooks/my-hook.sh"
}
Add to Existing settings.json
# Edit settings file
code .claude/settings.json # Or use your preferred editor
# Add hook configuration to "hooks" array
# If "hooks" doesn't exist, create it:
{
"hooks": [
{
"event": "PreToolUse",
"script": ".claude/hooks/pre-tool-component-validation.sh",
"matcher": {
"tool_name": ["Edit", "Write"],
"file_pattern": "**/{agents,commands,skills}/**/*.md"
}
}
]
}
Step 10: Test Hook Locally
Time: 5 minutes
Test your hook before activating it in Claude Code.
Manual Testing
Create test input:
# Test input JSON
cat > /tmp/hook-test-input.json << 'EOF'
{
"event": "PreToolUse",
"tool_name": "Edit",
"tool_input": {
"file_path": ".claude/agents/test-agent.md"
}
}
EOF
Run hook directly:
# Test shell hook
cat /tmp/hook-test-input.json | .claude/hooks/pre-tool-component-validation.sh
# Test Python hook
cat /tmp/hook-test-input.json | python3 .claude/hooks/pre-tool-component-validation.py
# Check exit code
echo "Exit code: $?"
Test Cases
Test 1: Valid component file
# Create valid agent file
cat > .claude/agents/test-agent.md << 'EOF'
---
name: test-agent
description: Test agent for validation
---
# Test Agent
EOF
# Run hook - should succeed (exit 0)
cat /tmp/hook-test-input.json | .claude/hooks/pre-tool-component-validation.sh
# Expected: {"continue": true, "message": "Component validation passed"}
# Exit code: 0
Test 2: Invalid component file (missing frontmatter)
# Create invalid agent file
cat > .claude/agents/invalid-agent.md << 'EOF'
# Invalid Agent
No frontmatter
EOF
# Update test input
cat > /tmp/hook-test-input.json << 'EOF'
{
"event": "PreToolUse",
"tool_name": "Edit",
"tool_input": {
"file_path": ".claude/agents/invalid-agent.md"
}
}
EOF
# Run hook - should block (exit 2)
cat /tmp/hook-test-input.json | .claude/hooks/pre-tool-component-validation.sh
# Expected: {"continue": false, "message": "Component file missing YAML frontmatter..."}
# Exit code: 2
Test 3: Non-component file
# Update test input to non-component file
cat > /tmp/hook-test-input.json << 'EOF'
{
"event": "PreToolUse",
"tool_name": "Edit",
"tool_input": {
"file_path": "README.md"
}
}
EOF
# Run hook - should allow (exit 0)
cat /tmp/hook-test-input.json | .claude/hooks/pre-tool-component-validation.sh
# Expected: {"continue": true, "message": "Not a component file"}
# Exit code: 0
Performance Testing
# Test hook execution time
time cat /tmp/hook-test-input.json | .claude/hooks/pre-tool-component-validation.sh
# Should complete in <500ms
# If >5s, optimize your hook logic
Step 11: Test Hook Integration
Time: 5 minutes
Test the hook in a real Claude Code session.
Enable Hook
- Ensure hook is configured in
.claude/settings.json - Start new Claude Code session
- Hook should load automatically
Verify Hook Loading
Check Claude Code logs for hook registration:
[Hooks] Loaded PreToolUse hook: pre-tool-component-validation.sh (project)
Integration Test
Test 1: Edit valid component
User: Edit the agents/test-agent.md file
Claude: [Hook runs, validates, allows]
Result: File edited successfully
Test 2: Edit invalid component
User: Edit the agents/invalid-agent.md file (no frontmatter)
Claude: [Hook runs, validates, blocks]
Result: Error message displayed to user:
"Operation blocked by hook: Component file missing YAML frontmatter opening delimiter (---)"
Test 3: Edit non-component file
User: Edit the README.md file
Claude: [Hook runs, sees non-component, allows]
Result: File edited successfully
Debug Hook Issues
Enable verbose logging:
{
"hooks": [
{
"event": "PreToolUse",
"script": ".claude/hooks/pre-tool-component-validation.sh",
"verbose": true
}
]
}
Check hook output:
# Hook stdout/stderr in Claude Code logs
tail -f ~/.claude/logs/hooks.log
Step 12: Document Hook Behavior
Time: 3 minutes
Document your hook so other developers understand its behavior.
Create Hook Documentation
File: .claude/hooks/README.md (or add to existing)
# Project Hooks
## pre-tool-component-validation.sh
**Event:** PreToolUse
**Purpose:** Validate CODITECT component files before modification
**Blocking:** Yes (exit 2 on validation failure)
### Triggers
- **Tools:** Edit, Write
- **Files:** agents/*.md, commands/*.md, skills/*/SKILL.md
### Validation Rules
Component files MUST have:
1. YAML frontmatter with `---` delimiters
2. Required field: `name`
3. Required field: `description`
### Behavior
- ✅ **Valid component:** Allow operation (exit 0)
- ❌ **Invalid component:** Block operation with error message (exit 2)
- ⚪ **Non-component file:** Allow operation (exit 0)
- ⚪ **New file creation:** Allow operation (exit 0)
### Error Messages
- "Component file missing YAML frontmatter opening delimiter (---)"
- "Component file missing YAML frontmatter closing delimiter (---)"
- "Component file missing required field: name"
- "Component file missing required field: description"
### Performance
- **Typical:** <200ms
- **Maximum:** 500ms
- **Timeout:** 5 seconds (hard limit)
### Dependencies
- `jq` (JSON parsing)
- `grep` (text matching)
- `sed` (text processing)
### Testing
```bash
# Run unit tests
./tests/test-component-validation-hook.sh
# Manual test
cat test-input.json | .claude/hooks/pre-tool-component-validation.sh
Maintenance
Owner: CODITECT Core Team Last Updated: December 3, 2025 Version: 1.0.0
### Add Inline Comments
Update your hook script with clear comments:
```bash
#!/bin/bash
# Hook: pre-tool-component-validation
# Purpose: Validate CODITECT component files before modification
# Event: PreToolUse
# Author: CODITECT Core Team
# Version: 1.0.0
#
# This hook enforces YAML frontmatter standards for CODITECT component files.
# It blocks Edit and Write operations on component files that don't have
# valid frontmatter with required fields.
#
# Exit Codes:
# 0 - Success (validation passed or not applicable)
# 2 - Blocking error (validation failed)
#
# Performance: <500ms typical, 5s timeout
set -euo pipefail
# ... rest of hook ...
Step 13: Validate and Commit
Time: 5 minutes
Validate your hook against the standard and commit to repository.
Validation Checklist
Use this checklist from CODITECT-STANDARD-HOOKS.md:
File Structure:
- Hook file in
.claude/hooks/directory - Filename follows
{event}-{purpose}.{ext}pattern (kebab-case) - File has proper shebang (
#!/bin/bashor#!/usr/bin/env python3) - File is executable (
chmod +x) - Filename under 64 characters
I/O Handling:
- Reads JSON input from stdin
- Parses JSON correctly with error handling
- Writes JSON output to stdout
- Uses stderr for warnings/errors
- Handles malformed JSON gracefully
Exit Codes:
- Returns 0 for success
- Returns 2 for blocking errors (PreToolUse, UserPromptSubmit, Stop events only)
- Returns 1 for warnings (non-blocking)
- Never returns other exit codes
Configuration:
- Hook registered in
.claude/settings.json - Event type specified correctly
- Script path is relative to project root
- Matcher configured (if needed)
- No syntax errors in JSON
Performance:
- Completes in <5 seconds (tested)
- Typical execution <1 second
- No blocking I/O operations
- No network requests (or timeout protected)
- Resource usage minimal (<50MB memory)
Security:
- Input validation for all external data
- Path validation (no directory traversal)
- No shell injection vulnerabilities
- No hardcoded secrets or credentials
- Fail open on unexpected errors
Documentation:
- Hook documented in
.claude/hooks/README.md - Purpose clearly stated
- Trigger conditions documented
- Error messages documented
- Dependencies listed
Testing:
- Unit tests passing (manual or automated)
- Integration tests passing
- Edge cases tested (empty input, malformed JSON, timeouts)
- Performance verified (<5s)
Calculate Quality Score
Grade A (90-100%): All 30+ checklist items passed Grade B (80-89%): 24-29 items passed (production-ready) Grade C (70-79%): 21-23 items passed (functional, needs improvement) Grade D (60-69%): 18-20 items passed (significant issues) Grade F (<60%): <18 items passed (major rework required)
Target: Grade B or higher
Commit to Repository
# Stage hook files
git add .claude/hooks/pre-tool-component-validation.sh
git add .claude/hooks/README.md
git add .claude/settings.json
# Commit with conventional format
git commit -m "feat: Add component validation PreToolUse hook
Add hook to validate CODITECT component files before modification.
**Hook Details:**
- Event: PreToolUse
- Purpose: Enforce YAML frontmatter standards
- Blocking: Yes (exit 2 on validation failure)
- Files: agents/*.md, commands/*.md, skills/*/SKILL.md
**Validation Rules:**
- YAML frontmatter with --- delimiters required
- Required fields: name, description
**Performance:**
- Typical: <200ms
- Maximum: 500ms
- Timeout: 5s
**Quality:** Grade A (100% checklist compliance)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>"
# Push to remote
git push
Complete Working Example
pre-tool-component-validation.sh
#!/bin/bash
# Hook: pre-tool-component-validation
# Purpose: Validate CODITECT component files before modification
# Event: PreToolUse
# Author: CODITECT Core Team
# Version: 1.0.0
set -euo pipefail
# Trap errors - fail open on unexpected errors
trap 'echo "{\"continue\": true, \"message\": \"Hook error: $?\"}" >&2; exit 0' ERR
# Check for required dependencies
if ! command -v jq &> /dev/null; then
echo '{"continue": true, "message": "Hook requires jq"}' >&2
exit 0
fi
# Read and parse JSON input
HOOK_INPUT=$(cat)
TOOL_NAME=$(echo "$HOOK_INPUT" | jq -r '.tool_name // empty')
FILE_PATH=$(echo "$HOOK_INPUT" | jq -r '.tool_input.file_path // empty')
# Validate input
if [ -z "$TOOL_NAME" ] || [ -z "$FILE_PATH" ]; then
echo '{"continue": true, "message": "Hook: missing input fields"}'
exit 0
fi
# Function: Check if file is a component file
is_component_file() {
local filepath="$1"
if [[ "$filepath" == *"/agents/"*.md ]] || \
[[ "$filepath" == *"/commands/"*.md ]] || \
[[ "$filepath" == *"/skills/"*"/SKILL.md" ]]; then
return 0
fi
return 1
}
# Function: Validate YAML frontmatter
validate_frontmatter() {
local filepath="$1"
# New file - allow creation
if [ ! -f "$filepath" ]; then
return 0
fi
# Read file content
local content=$(cat "$filepath")
# Check for opening delimiter
if ! echo "$content" | grep -q "^---"; then
echo '{"continue": false, "message": "Component file missing YAML frontmatter opening delimiter (---)"}'
return 2
fi
# Check for closing delimiter
if ! echo "$content" | sed -n '2,$p' | grep -q "^---"; then
echo '{"continue": false, "message": "Component file missing YAML frontmatter closing delimiter (---)"}'
return 2
fi
# Extract frontmatter
local frontmatter=$(echo "$content" | sed -n '/^---$/,/^---$/p' | sed '1d;$d')
# Check for required fields
if ! echo "$frontmatter" | grep -q "^name:"; then
echo '{"continue": false, "message": "Component file missing required field: name"}'
return 2
fi
if ! echo "$frontmatter" | grep -q "^description:"; then
echo '{"continue": false, "message": "Component file missing required field: description"}'
return 2
fi
return 0
}
# Main logic
if is_component_file "$FILE_PATH"; then
if ! validate_frontmatter "$FILE_PATH"; then
exit 2
fi
fi
# Success
echo '{"continue": true, "message": "Component validation passed"}'
exit 0
settings.json Configuration
{
"hooks": [
{
"event": "PreToolUse",
"script": ".claude/hooks/pre-tool-component-validation.sh",
"matcher": {
"tool_name": ["Edit", "Write"],
"file_pattern": "**/{agents,commands,skills}/**/*.md"
}
}
]
}
Quick Reference Checklist
Use this checklist when creating new hooks:
Planning Phase
- Choose appropriate hook event type
- Define hook purpose and scope
- Identify validation criteria
- Document exit code behavior
- List edge cases to handle
Implementation Phase
- Create hook file with proper naming
- Add shebang and imports
- Implement JSON input parsing
- Add error handling (fail open)
- Implement validation logic
- Add proper exit codes (0, 2, 1)
- Test performance (<5s)
Configuration Phase
- Add hook to settings.json
- Configure event type
- Add matcher (if needed)
- Verify JSON syntax
Testing Phase
- Test with valid input (exit 0)
- Test with invalid input (exit 2)
- Test with edge cases
- Test performance (<5s)
- Test in Claude Code session
Documentation Phase
- Document hook in README
- Add inline comments
- Document dependencies
- Document error messages
- Document performance characteristics
Validation Phase
- Run validation checklist (30+ items)
- Calculate quality score
- Achieve Grade B minimum
- Commit to repository
Common Pitfalls and Solutions
Pitfall 1: Hook Timeout
Problem: Hook takes >30 seconds and gets killed
Solution:
# Add timeout protection
timeout 5s bash -c 'your-hook-logic' || {
echo '{"continue": true, "message": "Hook timeout"}' >&2
exit 0
}
Pitfall 2: Invalid JSON Output
Problem: Hook outputs invalid JSON, breaks Claude Code
Solution:
import json
# Always use json.dumps()
result = {"continue": True, "message": "Success"}
print(json.dumps(result)) # Guaranteed valid JSON
Pitfall 3: Wrong Exit Code
Problem: Using exit 1 for blocking (should be exit 2)
Solution:
# WRONG
if validation_failed; then
exit 1 # This doesn't block!
fi
# CORRECT
if validation_failed; then
echo '{"continue": false, "message": "Validation failed"}'
exit 2 # This blocks the operation
fi
Pitfall 4: No Error Handling
Problem: Hook crashes on malformed input, breaks workflow
Solution:
# Add trap for errors
trap 'echo "{\"continue\": true, \"message\": \"Hook error\"}" >&2; exit 0' ERR
# Or wrap in try-catch (Python)
try:
main()
except Exception as e:
print(json.dumps({"continue": True, "message": f"Error: {e}"}), file=sys.stderr)
sys.exit(0)
Pitfall 5: Blocking Non-Blockable Events
Problem: Using exit 2 for PostToolUse (cannot block)
Solution:
# Check if event can block
if [ "$EVENT" = "PostToolUse" ]; then
# Cannot block - only use exit 0 or 1
echo '{"continue": true, "message": "Warning: issue detected"}' >&2
exit 1 # Warning, not blocking
else
# Can block
echo '{"continue": false, "message": "Validation failed"}'
exit 2
fi
Next Steps
After completing this guide:
- Read the standard: CODITECT-STANDARD-HOOKS.md
- Review examples: Check
.claude/hooks/in other CODITECT projects - Explore advanced patterns: Multi-tool orchestration, caching, optimization
- Share your hooks: Contribute useful hooks back to CODITECT community
Advanced Topics:
- Performance optimization (caching, memoization)
- Multi-hook coordination
- Hook testing frameworks
- Monitoring and observability
- Security hardening
Congratulations! You've created a production-ready CODITECT hook. 🎉
Quality Check: Did your hook achieve Grade B (80%) or higher? ✅
Next: Create more hooks to automate your workflow and enforce standards!