Event-Driven Automation Infrastructure
Location: ${PAI_DIR}/Hooks/
Configuration: ${PAI_DIR}/settings.json
Status: Active - All hooks running in production
The PAI hook system is an event-driven automation infrastructure built on Claude Code's native hook support. Hooks are executable scripts (TypeScript/Python) that run automatically in response to specific events during Claude Code sessions.
Core Capabilities:
- Session Management - Auto-load context, capture summaries, manage state
- Voice Notifications - Text-to-speech announcements for task completions
- History Capture - Automatic work/learning documentation to
${PAI_DIR}/History/ - Multi-Agent Support - Agent-specific hooks with voice routing
- Observability - Real-time event streaming to dashboard
- Tab Titles - Dynamic terminal tab updates with task context
Key Principle: Hooks run asynchronously and fail gracefully. They enhance the user experience but never block Claude Code's core functionality.
Claude Code supports the following hook events (from ${PAI_DIR}/Hooks/lib/observability.ts):
When: Claude Code session begins (new conversation) Use Cases:
- Load PAI context from
skills/CORE/SKILL.md - Initialize session state
- Capture session metadata
Current Hooks:
{
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "${PAI_DIR}/Hooks/load-core-context.ts"
},
{
"type": "command",
"command": "${PAI_DIR}/Hooks/initialize-pai-session.ts"
},
{
"type": "command",
"command": "${PAI_DIR}/Hooks/capture-all-events.ts --event-type SessionStart"
}
]
}
]
}What They Do:
load-core-context.ts- Readsskills/CORE/SKILL.mdand injects PAI context as<system-reminder>at session startinitialize-pai-session.ts- Sets up session state and environmentcapture-all-events.ts- Logs event to${PAI_DIR}/History/raw-outputs/YYYY-MM/YYYY-MM-DD_all-events.jsonl
When: Claude Code session terminates (conversation ends) Use Cases:
- Generate session summaries
- Save session metadata
- Cleanup temporary state
Current Hooks:
{
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "${PAI_DIR}/Hooks/capture-session-summary.ts"
},
{
"type": "command",
"command": "${PAI_DIR}/Hooks/capture-all-events.ts --event-type SessionEnd"
}
]
}
]
}What They Do:
capture-session-summary.ts- Analyzes session activity and creates summary document in${PAI_DIR}/History/sessions/YYYY-MM/- Captures: files changed, commands executed, tools used, session focus, duration
When: User submits a new prompt to Claude Use Cases:
- Update UI indicators
- Pre-process user input
- Capture prompts for analysis
Current Hooks:
{
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "${PAI_DIR}/Hooks/update-tab-titles.ts"
},
{
"type": "command",
"command": "${PAI_DIR}/Hooks/capture-all-events.ts --event-type UserPromptSubmit"
}
]
}
]
}What They Do:
update-tab-titles.ts- Updates Kitty terminal tab title with task summary- Launches background Haiku summarization for better tab titles
- Sets
♻️emoji prefix to indicate processing
When: Main agent (Kai) completes a response Use Cases:
- Voice notifications for task completion
- Capture work summaries and learnings
- Update terminal tab with completion status
Current Hooks:
{
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "${PAI_DIR}/Hooks/stop-hook.ts"
},
{
"type": "command",
"command": "${PAI_DIR}/Hooks/capture-all-events.ts --event-type Stop"
}
]
}
]
}What They Do:
stop-hook.ts- THE CRITICAL HOOK for main agent completions- Extracts
🎯 COMPLETED:line from response - Sends to voice server with PAI's voice ID (
s3TPKV1kjDlVtZbl4Ksh) - Captures work summaries to
${PAI_DIR}/History/sessions/YYYY-MM/or learnings to${PAI_DIR}/History/learnings/YYYY-MM/ - Updates Kitty tab with
✅prefix - Sends event to observability dashboard
- Extracts
Learning Detection: Automatically identifies learning moments (2+ indicators: problem/issue/bug, fixed/solved, troubleshoot/debug, lesson/takeaway)
When: Subagent (Task tool) completes execution Use Cases:
- Agent-specific voice notifications
- Capture agent outputs
- Track multi-agent workflows
Current Hooks:
{
"SubagentStop": [
{
"hooks": [
{
"type": "command",
"command": "${PAI_DIR}/Hooks/subagent-stop-hook.ts"
},
{
"type": "command",
"command": "${PAI_DIR}/Hooks/capture-all-events.ts --event-type SubagentStop"
}
]
}
]
}What They Do:
subagent-stop-hook.ts- Agent-specific completion handling- Waits for Task tool result in transcript
- Extracts
[AGENT:type]tag and completion message - Routes to agent-specific voice (via agent's own voice notification in response)
- Captures agent output to appropriate history category
- Sends to observability dashboard
Agent-Specific Routing:
[AGENT:engineer]→ Engineer voice ID[AGENT:researcher]→ Researcher voice ID[AGENT:pentester]→ Pentester voice ID- etc.
When: Before Claude executes any tool Use Cases:
- Tool usage analytics
- Pre-execution validation
- Performance monitoring
Current Hooks:
{
"PreToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "${PAI_DIR}/Hooks/capture-all-events.ts --event-type PreToolUse"
}
]
}
]
}What They Do:
- Captures tool name, input parameters, timestamp
- Logs to daily events file for analysis
When: After Claude executes any tool Use Cases:
- Capture tool outputs
- Error tracking
- Performance metrics
Current Hooks:
{
"PostToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "${PAI_DIR}/Hooks/capture-all-events.ts --event-type PostToolUse"
}
]
}
]
}What They Do:
- Captures tool output, execution time, success/failure
- Logs to
${PAI_DIR}/History/raw-outputs/YYYY-MM/YYYY-MM-DD_all-events.jsonl - Powers observability dashboard
When: Before Claude compacts context (long conversations) Use Cases:
- Preserve important context
- Log compaction events
- Pre-compaction cleanup
Current Hooks:
{
"PreCompact": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "${PAI_DIR}/Hooks/context-compression-hook.ts"
},
{
"type": "command",
"command": "${PAI_DIR}/Hooks/capture-all-events.ts --event-type PreCompact"
}
]
}
]
}What They Do:
context-compression-hook.ts- Handles context preservation before compaction- Captures metadata about compaction events
File: ${PAI_DIR}/settings.json
Section: "hooks": { ... }
Hooks have access to all environment variables from ${PAI_DIR}/settings.json "env" section:
{
"env": {
"PAI_DIR": "/Users/daniel/.claude",
"DA": "Kai",
"MCP_API_KEY": "...",
"CLAUDE_CODE_MAX_OUTPUT_TOKENS": "64000"
}
}Key Variables:
PAI_DIR- PAI installation directory (always/Users/daniel/.claude)DA- Digital Assistant name ("Kai")- Hook scripts reference
${PAI_DIR}in command paths
{
"hooks": {
"HookEventName": [
{
"matcher": "pattern", // Optional: filter which tools/events trigger hook
"hooks": [
{
"type": "command",
"command": "${PAI_DIR}/Hooks/my-hook.ts --arg value"
}
]
}
]
}
}Fields:
HookEventName- One of: SessionStart, SessionEnd, UserPromptSubmit, Stop, SubagentStop, PreToolUse, PostToolUse, PreCompactmatcher- Pattern to match (use"*"for all tools, or specific tool names)type- Always"command"(executes external script)command- Path to executable hook script (TypeScript/Python/Bash)
All hooks receive JSON data on stdin:
{
session_id: string; // Unique session identifier
transcript_path: string; // Path to JSONL transcript
hook_event_name: string; // Event that triggered hook
prompt?: string; // User prompt (UserPromptSubmit only)
tool_name?: string; // Tool name (PreToolUse/PostToolUse)
tool_input?: any; // Tool parameters (PreToolUse)
tool_output?: any; // Tool result (PostToolUse)
// ... event-specific fields
}Pattern: Extract completion message → Send to voice server
// stop-hook.ts pattern
const completionMessage = extractKaiCompletion(lastMessage);
const payload = {
title: 'PAI',
message: completionMessage,
voice_enabled: true,
voice_id: 's3TPKV1kjDlVtZbl4Ksh' // PAI's ElevenLabs voice
};
await fetch('http://localhost:8888/notify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});Agent-Specific Voices:
- Main agent (PAI):
s3TPKV1kjDlVtZbl4Ksh - Engineer:
fATgBRI8wg5KkDFg8vBd - Researcher:
AXdMgz6evoL7OPd7eU12 - Pentester:
xvHLFjaUEpx4BOf7EiDd - Intern:
d3MFdIuCfbAIwiu7jC4a
See skills/CORE/SKILL.md for complete voice ID mapping.
Pattern: Parse structured response → Save to appropriate history directory
File Naming Convention:
YYYY-MM-DD-HHMMSS_TYPE_description.md
Types:
WORK- General task completionsLEARNING- Problem-solving learningsSESSION- Session summariesRESEARCH- Research findings (from agents)FEATURE- Feature implementations (from agents)DECISION- Architectural decisions (from agents)
Example from stop-hook.ts:
const structured = extractStructuredSections(lastMessage);
const isLearning = isLearningCapture(text, structured);
const captureType = isLearning ? 'LEARNING' : 'WORK';
const targetDir = isLearning
? join(baseDir, 'history', 'learnings', yearMonth)
: join(baseDir, 'history', 'sessions', yearMonth);
const filename = generateFilename(description, captureType);
writeFileSync(join(targetDir, filename), content);Structured Sections Parsed:
📋 SUMMARY:- Brief overview🔍 ANALYSIS:- Key findings⚡ ACTIONS:- Steps taken✅ RESULTS:- Outcomes📊 STATUS:- Current state➡️ NEXT:- Follow-up actions🎯 COMPLETED:- Voice notification line
Pattern: Identify which agent is executing → Route appropriately
// From capture-all-events.ts
let agentName = getAgentForSession(sessionId);
// Detect from Task tool
if (hookData.tool_name === 'Task' && hookData.tool_input?.subagent_type) {
agentName = hookData.tool_input.subagent_type;
setAgentForSession(sessionId, agentName);
}
// Detect from CLAUDE_CODE_AGENT env variable
else if (process.env.CLAUDE_CODE_AGENT) {
agentName = process.env.CLAUDE_CODE_AGENT;
}
// Detect from path (subagents run in /Agents/name/)
else if (hookData.cwd && hookData.cwd.includes('/Agents/')) {
const agentMatch = hookData.cwd.match(/\/agents\/([^\/]+)/);
if (agentMatch) agentName = agentMatch[1];
}Session Mapping: ${PAI_DIR}/agent-sessions.json
{
"session-id-abc123": "engineer",
"session-id-def456": "researcher"
}Pattern: Send event to dashboard → Fail silently if offline
import { sendEventToObservability, getCurrentTimestamp, getSourceApp } from './lib/observability';
await sendEventToObservability({
source_app: getSourceApp(), // 'PAI' or agent name
session_id: hookInput.session_id,
hook_event_type: 'Stop',
timestamp: getCurrentTimestamp(),
transcript_path: hookInput.transcript_path,
summary: completionMessage,
// ... additional fields
}).catch(() => {
// Silently fail - dashboard may not be running
});Dashboard URLs:
- Server:
http://localhost:4000 - Client:
http://localhost:5173
Pattern: Hook executes quickly → Launch background processes for slow operations
// update-tab-titles.ts pattern
// Set immediate tab title (fast)
execSync(`printf '\\033]0;${titleWithEmoji}\\007' >&2`);
// Launch background process for Haiku summary (slow)
Bun.spawn(['bun', `${paiDir}/Hooks/update-tab-title.ts`, prompt], {
stdout: 'ignore',
stderr: 'ignore',
stdin: 'ignore'
});
process.exit(0); // Exit immediatelyKey Principle: Hooks must never block Claude Code. Always exit quickly, use background processes for slow work.
Pattern: Wrap everything in try/catch → Log errors → Exit successfully
async function main() {
try {
// Hook logic here
} catch (error) {
// Log but don't fail
console.error('Hook error:', error);
}
process.exit(0); // Always exit 0
}Why: If hooks crash, Claude Code may freeze. Always exit cleanly.
Decide which event should trigger your hook (SessionStart, Stop, PostToolUse, etc.)
Location: ${PAI_DIR}/Hooks/my-custom-hook.ts
Template:
#!/usr/bin/env bun
interface HookInput {
session_id: string;
transcript_path: string;
hook_event_name: string;
// ... event-specific fields
}
async function main() {
try {
// Read stdin
const input = await Bun.stdin.text();
const data: HookInput = JSON.parse(input);
// Your hook logic here
console.log(`Hook triggered: ${data.hook_event_name}`);
// Example: Read transcript
const fs = require('fs');
const transcript = fs.readFileSync(data.transcript_path, 'utf-8');
// Do something with the data
} catch (error) {
// Log but don't fail
console.error('Hook error:', error);
}
process.exit(0); // Always exit 0
}
main();chmod +x ${PAI_DIR}/Hooks/my-custom-hook.ts{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "${PAI_DIR}/Hooks/my-custom-hook.ts"
}
]
}
]
}
}# Test hook directly
echo '{"session_id":"test","transcript_path":"/tmp/test.jsonl","hook_event_name":"Stop"}' | bun ${PAI_DIR}/Hooks/my-custom-hook.tsHooks are loaded at startup. Restart to apply changes.
- Hooks should complete in < 500ms
- Use background processes for slow work (Haiku API calls, file processing)
- Exit immediately after launching background work
- Always wrap in try/catch
- Log errors to stderr (available in hook debug logs)
- Always
process.exit(0)- never throw or exit(1)
- Never wait for external services (unless they respond quickly)
- Use
.catch(() => {})for async operations - Fail silently if optional services are offline
- Use timeout when reading stdin (Claude Code may not send data immediately)
- Handle empty/invalid input gracefully
const decoder = new TextDecoder();
const reader = Bun.stdin.stream().getReader();
const timeoutPromise = new Promise<void>((resolve) => {
setTimeout(() => resolve(), 500); // 500ms timeout
});
await Promise.race([readPromise, timeoutPromise]);- Check
existsSync()before reading files - Create directories with
{ recursive: true } - Use PST timestamps for consistency
- All
settings.jsonenv vars available viaprocess.env - Use
${PAI_DIR}in settings.json for portability - Access in code via
process.env.PAI_DIR
- Send events to dashboard for visibility
- Include all relevant metadata (session_id, tool_name, etc.)
- Use
.catch(() => {})- dashboard may be offline
Check:
- Is hook script executable?
chmod +x ${PAI_DIR}/Hooks/my-hook.ts - Is path correct in settings.json? Use
${PAI_DIR}/Hooks/... - Is settings.json valid JSON?
jq . ${PAI_DIR}/settings.json - Did you restart Claude Code after editing settings.json?
Debug:
# Test hook directly
echo '{"session_id":"test","transcript_path":"/tmp/test.jsonl","hook_event_name":"Stop"}' | bun ${PAI_DIR}/Hooks/my-hook.ts
# Check hook logs (stderr output)
tail -f ${PAI_DIR}/Hooks/debug.log # If you add loggingCause: Hook not exiting (infinite loop, waiting for input, blocking operation)
Fix:
- Add timeouts to all blocking operations
- Ensure
process.exit(0)is always reached - Use background processes for long operations
- Check stdin reading has timeout
Prevention:
// Always use timeout
setTimeout(() => {
console.error('Hook timeout - exiting');
process.exit(0);
}, 5000); // 5 second maxCheck:
- Is voice server running?
curl http://localhost:8888/health - Is voice_id correct? See
skills/CORE/SKILL.mdfor mappings - Is message format correct?
{"message":"...", "voice_id":"...", "title":"..."} - Is ElevenLabs API key in
${PAI_DIR}/.env?
Debug:
# Test voice server directly
curl -X POST http://localhost:8888/notify \
-H "Content-Type: application/json" \
-d '{"message":"Test message","voice_id":"s3TPKV1kjDlVtZbl4Ksh","title":"Test"}'Common Issues:
- Wrong voice_id → Silent failure (invalid ID)
- Voice server offline → Hook continues (graceful failure)
- No
🎯 COMPLETED:line → No voice notification extracted
Check:
- Does
${PAI_DIR}/History/directory exist? - Are structured sections present in response? (
📋 SUMMARY:,🎯 COMPLETED:, etc.) - Is hook actually running? Check
${PAI_DIR}/History/raw-outputs/for events - File permissions?
ls -la ${PAI_DIR}/History/sessions/
Debug:
# Check recent captures
ls -lt ${PAI_DIR}/History/sessions/$(date +%Y-%m)/ | head -10
ls -lt ${PAI_DIR}/History/learnings/$(date +%Y-%m)/ | head -10
# Check raw events
tail ${PAI_DIR}/History/raw-outputs/$(date +%Y-%m)/$(date +%Y-%m-%d)_all-events.jsonlCommon Issues:
- Missing structured format → No parsing
- No
🎯 COMPLETED:line → Capture may fail - Learning detection too strict → Adjust
isLearningCapture()logic
Symptom: Stop hook configured and working, but Stop events not firing consistently
Evidence:
# Check if Stop events fired today
grep '"event_type":"Stop"' ${PAI_DIR}/History/raw-outputs/$(date +%Y-%m)/$(date +%Y-%m-%d)_all-events.jsonl
# Result: 0 matches (no Stop events)
# But other hooks ARE working
grep '"event_type":"PostToolUse"' ${PAI_DIR}/History/raw-outputs/$(date +%Y-%m)/$(date +%Y-%m-%d)_all-events.jsonl
# Result: 80+ matches (PostToolUse working fine)Impact:
- Automatic work summaries NOT captured to history (despite Stop hook logic being correct)
- Learning moments NOT auto-detected
- Voice notifications from main agent responses NOT sent
- Manual verification and capture REQUIRED
Root Cause:
- Claude Code event trigger issue (external to hook system)
- Stop event not being emitted when main agent completes responses
- Hook configuration is correct, hook script works, event just never fires
- Other event types (PostToolUse, SessionEnd, UserPromptSubmit) work fine
Workaround (MANDATORY):
-
Added CAPTURE field to response format (see
${PAI_DIR}/Skills/CORE/SKILL.md)- MANDATORY field in every response
- Forces verification before completing responses
- Must document: "Auto-captured" / "Manually saved" / "N/A"
-
Added MANDATORY VERIFICATION GATE to file organization section
- Before completing valuable work, MUST run verification commands
- Check if auto-capture happened (ls -lt history directories)
- If not, manually save to appropriate history location
-
Verification Commands:
# Check if auto-captured (should happen, but often doesn't due to Stop event bug) ls -lt ${PAI_DIR}/History/sessions/$(date +%Y-%m)/ | head -5 ls -lt ${PAI_DIR}/History/learnings/$(date +%Y-%m)/ | head -5 # If 0 results or doesn't match current work → Manual capture required
Status: UNRESOLVED (Claude Code issue, not hook configuration)
Mitigation: Structural enforcement via response format (cannot complete valuable work without verification)
Tracking: Documented in ${PAI_DIR}/Skills/CORE/SKILL.md (History Capture System section)
Long-term Fix:
- Report to Anthropic (Claude Code team) as Stop event reliability issue
- Monitor future Claude Code updates for fix
- Keep workaround in place until Stop events fire reliably
Check:
- Is
${PAI_DIR}/agent-sessions.jsonwritable? - Is
[AGENT:type]tag in🎯 COMPLETED:line? - Is agent running from correct directory? (
/Agents/name/)
Debug:
# Check session mappings
cat ${PAI_DIR}/agent-sessions.json | jq .
# Check subagent-stop debug log
tail -f ${PAI_DIR}/Hooks/subagent-stop-debug.logFix:
- Ensure agents include
[AGENT:type]in completion line - Verify Task tool passes
subagent_typeparameter - Check cwd includes
/Agents/in path
Check:
- Is dashboard server running?
curl http://localhost:4000/health - Are hooks sending events? Check
sendEventToObservability()calls - Network issues?
netstat -an | grep 4000
Debug:
# Start dashboard server
cd ${PAI_DIR}/Skills/system/observability/dashboard/apps/server
bun run dev
# Check server logs
# Events should appear in real-timeNote: Hooks fail silently if dashboard offline (by design). Not critical for operation.
Check:
- Does
${PAI_DIR}/Skills/CORE/SKILL.mdexist? - Is
load-core-context.tsexecutable? - Is
PAI_DIRenv variable set correctly?
Debug:
# Test context loading directly
bun ${PAI_DIR}/Hooks/load-core-context.ts
# Should output <system-reminder> with SKILL.md contentCommon Issues:
- Subagent sessions loading main context → Fixed (subagent detection in hook)
- File not found → Check
PAI_DIRenvironment variable - Permission denied →
chmod +x ${PAI_DIR}/Hooks/load-core-context.ts
Hooks in same event execute sequentially in order defined in settings.json:
{
"Stop": [
{
"hooks": [
{ "command": "${PAI_DIR}/Hooks/stop-hook.ts" }, // Runs first
{ "command": "${PAI_DIR}/Hooks/capture-all-events.ts" } // Runs second
]
}
]
}Note: If first hook hangs, second won't run. Keep hooks fast!
"matcher" field filters which events trigger hook:
{
"PostToolUse": [
{
"matcher": "Bash", // Only Bash tool executions
"hooks": [...]
},
{
"matcher": "*", // All tool executions
"hooks": [...]
}
]
}Patterns:
"*"- All events"Bash"- Specific tool name""- Empty (all events, same as*)
SessionStart:
{
session_id: string;
transcript_path: string;
hook_event_name: "SessionStart";
cwd: string;
}UserPromptSubmit:
{
session_id: string;
transcript_path: string;
hook_event_name: "UserPromptSubmit";
prompt: string; // The user's prompt text
}PreToolUse:
{
session_id: string;
transcript_path: string;
hook_event_name: "PreToolUse";
tool_name: string;
tool_input: any; // Tool parameters
}PostToolUse:
{
session_id: string;
transcript_path: string;
hook_event_name: "PostToolUse";
tool_name: string;
tool_input: any;
tool_output: any; // Tool result
error?: string; // If tool failed
}Stop:
{
session_id: string;
transcript_path: string;
hook_event_name: "Stop";
}SubagentStop:
{
session_id: string;
transcript_path: string;
hook_event_name: "SubagentStop";
}SessionEnd:
{
conversation_id: string; // Note: different field name
timestamp: string;
}- Voice System:
${PAI_DIR}/voice-server/USAGE.md - Agent Architecture:
${PAI_DIR}/Skills/CORE/agent-protocols.md - History/UOCS:
${PAI_DIR}/Skills/CORE/history-system.md - Observability Dashboard:
${PAI_DIR}/Skills/Observability/
HOOK LIFECYCLE:
1. Event occurs (SessionStart, Stop, etc.)
2. Claude Code writes hook data to stdin
3. Hook script executes
4. Hook reads stdin (with timeout)
5. Hook performs actions (voice, capture, etc.)
6. Hook exits 0 (always succeeds)
7. Claude Code continues
KEY FILES:
${PAI_DIR}/settings.json Hook configuration
${PAI_DIR}/Hooks/ Hook scripts
${PAI_DIR}/Hooks/lib/observability.ts Helper library
${PAI_DIR}/History/raw-outputs/ Event logs (JSONL)
${PAI_DIR}/History/sessions/ Work summaries
${PAI_DIR}/History/learnings/ Learning captures
${PAI_DIR}/agent-sessions.json Session→Agent mapping
CRITICAL HOOKS:
stop-hook.ts Voice + history capture (main agent)
subagent-stop-hook.ts Voice + history capture (subagents)
load-core-context.ts PAI context loading
capture-all-events.ts Universal event logger
VOICE SERVER:
URL: http://localhost:8888/notify
Payload: {"message":"...", "voice_id":"...", "title":"..."}
Main Voice: s3TPKV1kjDlVtZbl4Ksh (PAI)
OBSERVABILITY:
Server: http://localhost:4000
Client: http://localhost:5173
Events: All hooks send to /events endpoint
Last Updated: 2025-11-01 Status: Production - All hooks active and tested Maintainer: Daniel Miessler (maintainer@example.com)