Skip to main content

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
  • jq installed (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

EventCan BlockTypical DurationBest For
PreToolUse✅ Yes<2sValidation, security checks
PostToolUse❌ No<3sFormatting, cleanup
UserPromptSubmit✅ Yes<1sPrompt enhancement
SessionStart❌ No<5sInitialization, context loading
SessionEnd❌ No<5sCleanup, 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

  1. What problem does this solve?

    • Example: "Prevent corruption of CODITECT component files by enforcing YAML frontmatter"
  2. Which tools trigger this hook?

    • Write, Edit tools when targeting component files
  3. 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)
  4. What happens on success vs. failure?

    • Success: Allow tool to proceed (exit 0)
    • Failure: Block tool with helpful error message (exit 2)
  5. 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}

ComponentRuleExample
EventLowercase, hyphenatedpre-tool, post-tool, user-prompt
PurposeDescriptive, 2-4 wordscomponent-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 CodeMeaningHook BehaviorUse When
0SuccessAllow operation to proceedValidation passed or hook doesn't apply
2Blocking errorBLOCK operationCritical validation failure
1WarningAllow with warning loggedNon-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

FieldTypeRequiredDescription
eventstring✅ YesHook event type (PreToolUse, PostToolUse, etc.)
scriptstring✅ YesPath to hook script (relative to project root)
matcherobject❌ NoConditions for when hook runs
matcher.tool_namearray❌ NoTool names to match (Edit, Write, Read, etc.)
matcher.file_patternstring❌ NoGlob 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

  1. Ensure hook is configured in .claude/settings.json
  2. Start new Claude Code session
  3. 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/bash or #!/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:

  1. Read the standard: CODITECT-STANDARD-HOOKS.md
  2. Review examples: Check .claude/hooks/ in other CODITECT projects
  3. Explore advanced patterns: Multi-tool orchestration, caching, optimization
  4. 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!