ADR-183: Governance Hook Architecture
Status
ACCEPTED - January 10, 2026
Context
Current State
Claude Code provides a hook system for extending functionality:
- PreToolUse: Validates or modifies tool calls before execution
- PostToolUse: Processes results after tool execution
- SessionStart: Runs at session initialization
- SessionEnd: Runs at session termination
CODITECT uses these hooks for governance enforcement, but the architecture was undocumented.
Problem Statement
- Missing Documentation: No ADR documenting hook architecture
- Scattered Implementation: Hooks spread across multiple files
- Inconsistent Patterns: Different validation approaches
- No Error Handling Standard: Hooks fail inconsistently
- SessionStart Redundancy: Overlaps with CLAUDE.md auto-loading
Requirements
- Hook Catalog: Document all governance hooks
- Consistent Patterns: Standard hook implementation
- Error Handling: Fail-open vs fail-closed policies
- Environment Control: Enable/disable via environment variables
- Logging Standard: Centralized hook logging
Decision
We adopt a Layered Governance Hook Architecture:
Hook Types
| Hook Type | Timing | Purpose | Fail Mode |
|---|---|---|---|
| PreToolUse | Before execution | Validate, block, modify | Configurable |
| PostToolUse | After execution | Sync, log, notify | Fail-open |
| SessionStart | Session init | DEPRECATED | N/A |
| SessionEnd | Session close | Export, backup | Fail-open |
Governance Hooks Catalog
PreToolUse Hooks
| Hook | File | Purpose | Validated Tools |
|---|---|---|---|
| Task ID Validator | task-id-validator.py | Enforce task ID in descriptions | Bash, Edit, Write, Read, Grep, Glob, Task |
| Task Tracking Enforcer | task-tracking-enforcer.py | Validate track nomenclature on TodoWrite | TodoWrite |
| Install Script Validator | validators/install-script-validator.py | Validate installation scripts | Bash |
| Skill Checker | skill-checker.py | Validate skill invocations | Skill |
| Governance Enforcer | governance-enforcer.py | General governance rules | Multiple |
PostToolUse Hooks
| Hook | File | Purpose | Triggered By |
|---|---|---|---|
| Task Plan Sync | task-plan-sync.py | Sync TodoWrite to PILOT + Cloud | TodoWrite |
| Session Log Sync | session-log-sync.py | Auto-append to daily session log | Multiple |
SessionEnd Hooks
| Hook | File | Purpose |
|---|---|---|
| Session Retrospective | session-retrospective.py | Analyze session for patterns |
| Auto Export | auto-export.py | Export session before close |
SessionStart Deprecation
SessionStart hooks are deprecated because:
- CLAUDE.md auto-loads at session start (automatic)
- SessionStart hooks cause race conditions with MCP servers
- Redundant validation already in PreToolUse hooks
Migration: Move all SessionStart logic to CLAUDE.md or PreToolUse hooks.
Implementation Pattern
All hooks MUST follow this structure:
#!/usr/bin/env python3
"""
CODITECT {Hook Type} {Hook Name} Hook (ADR-183)
{Brief description of purpose}
Hook Type: {PreToolUse|PostToolUse|SessionEnd}
Input: JSON from stdin with {relevant fields}
Output: JSON with continue: true/false
Environment Variables:
CODITECT_SKIP_{HOOK_NAME}=1 - Skip this hook
CODITECT_{HOOK_NAME}_WARN_ONLY=1 - Warn but don't block
Author: AZ1.AI INC
Framework: CODITECT
ADR: ADR-183 Governance Hook Architecture
"""
import json
import os
import sys
from typing import Tuple
# Import centralized hook logger
try:
from hook_logger import get_hook_logger
logger = get_hook_logger("{hook_name}")
except ImportError:
logger = None
# Environment variable controls
ENV_SKIP = "CODITECT_SKIP_{HOOK_NAME}"
ENV_WARN_ONLY = "CODITECT_{HOOK_NAME}_WARN_ONLY"
def is_disabled() -> bool:
"""Check if hook is disabled via environment variable."""
return os.environ.get(ENV_SKIP, "").lower() in ("1", "true", "yes")
def is_warn_only() -> bool:
"""Check if running in warn-only mode."""
return os.environ.get(ENV_WARN_ONLY, "").lower() in ("1", "true", "yes")
def validate(input_data: dict) -> Tuple[bool, str]:
"""
Main validation logic.
Returns:
Tuple of (is_valid, message)
"""
# Hook-specific validation logic here
return True, ""
def main() -> None:
"""Main hook entry point."""
if is_disabled():
print(json.dumps({"continue": True}))
return
try:
input_data = json.load(sys.stdin)
is_valid, message = validate(input_data)
if is_valid:
print(json.dumps({"continue": True}))
elif is_warn_only():
print(json.dumps({
"continue": True,
"hookSpecificOutput": {"message": f"⚠️ WARNING: {message}"}
}))
else:
print(json.dumps({
"continue": False,
"hookSpecificOutput": {"message": f"🚫 BLOCKED: {message}"}
}))
except Exception:
# Fail-open on errors
print(json.dumps({"continue": True}))
if __name__ == "__main__":
main()
Environment Variable Convention
| Variable Pattern | Purpose |
|---|---|
CODITECT_SKIP_{HOOK}=1 | Completely skip the hook |
CODITECT_{HOOK}_WARN_ONLY=1 | Warn but don't block |
CODITECT_HOOKS_DISABLED=1 | Disable ALL hooks |
CODITECT_HOOKS_VERBOSE=1 | Enable verbose logging |
Logging Standard
All hooks use centralized logging via hook_logger.py:
from hook_logger import get_hook_logger
logger = get_hook_logger("hook_name")
# Standard log events
logger.log_hook_start(tool_name=..., description_preview=...)
logger.log_hook_end(success=True, decision="allow|warn|block")
logger.log_hook_error(exception, context="...")
Log Location: ~/PROJECTS/.coditect-data/logs/hooks/
Hook Registration
Hooks are registered in ~/.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": {"tool_name": "Bash|Edit|Write|Read|Grep|Glob|Task"},
"hooks": [
{"type": "command", "command": "python3 ~/.coditect/hooks/task-id-validator.py"}
]
},
{
"matcher": {"tool_name": "TodoWrite"},
"hooks": [
{"type": "command", "command": "python3 ~/.coditect/hooks/task-tracking-enforcer.py"}
]
}
],
"PostToolUse": [
{
"matcher": {"tool_name": "TodoWrite"},
"hooks": [
{"type": "command", "command": "python3 ~/.coditect/hooks/task-plan-sync.py"}
]
}
]
}
}
Consequences
Positive
- Documented Architecture: Clear hook catalog and patterns
- Consistent Implementation: Standard template for all hooks
- Environment Control: Hooks can be disabled for debugging
- Centralized Logging: All hooks log to standard location
- Fail-Open Default: Errors don't block work
Negative
- Maintenance Overhead: Must keep hook catalog updated
- Performance: Each hook adds latency to tool calls
- Complexity: New contributors must understand hook system
Mitigations
- Hook Catalog: This ADR serves as documentation
- Minimal Hooks: Only add hooks with clear governance value
- Onboarding: CLAUDE.md references this ADR
Implementation
Phase 1: Documentation (Complete)
- Create ADR-183 (this document)
- Document existing hooks
- Define implementation pattern
Phase 2: Standardization (In Progress)
- Update all hooks to follow pattern
- Add environment variable support to all hooks
- Implement centralized logging
Phase 3: Validation (Pending)
- Fix task-id-validator.py regex (see ADR-054)
- Add integration tests for hooks
- Create hook health dashboard
Related
- ADR-054: Track Nomenclature Extensibility (defines valid track patterns)
- ADR-056: Action-Level Task Tracking Protocol (defines task ID requirement)
- ADR-116: Track-Based Plan Architecture (defines TRACK file structure)
- Standard: CODITECT-STANDARD-AUTOMATION.md (Principle #12)
- Skill: coditect-governance
Version: 1.0.0 Author: Hal Casteel Approved By: Platform Architecture Team