/** * Claude Code Hook System - Event-based Plugin Hooks * * Features: * - Multiple hook types (pre/post events) * - Priority-based execution * - Async hook support * - Error handling and recovery * - Hook chaining */ import fs from 'fs/promises' import path from 'path' import { spawn } from 'child_process' // ============================================================================ // TYPES AND INTERFACES // ============================================================================ export type HookEvent = | 'UserPromptSubmit' | 'UserPromptSubmit-hook' | 'PreToolUse' | 'PostToolUse' | 'PreFileEdit' | 'PostFileEdit' | 'PreCommand' | 'PostCommand' | 'SessionStart' | 'SessionEnd' | 'PluginLoad' | 'PluginUnload' | 'Error' export interface HookContext { event: HookEvent timestamp: string sessionId?: string messageId?: string data: Record } export interface HookResult { success: boolean data?: any error?: string modifications?: { args?: any output?: any cancel?: boolean } } export interface HookDefinition { type: 'command' | 'module' command?: string module?: string handler?: string timeout?: number priority?: number condition?: string enabled?: boolean } export interface HookConfig { description?: string hooks: Record } // ============================================================================ // HOOK SYSTEM CLASS // ============================================================================ export class HookSystem { private hooksFile: string private hooksDir: string private hooks: Map = new Map() private hookResults: Map = new Map() constructor(claudeDir: string = path.join(process.env.HOME || '', '.claude')) { this.hooksDir = path.join(claudeDir, 'hooks') this.hooksFile = path.join(claudeDir, 'hooks.json') } // ============================================================================ // INITIALIZATION // ============================================================================ async initialize(): Promise { await this.ensureDirectories() await this.loadHooks() } private async ensureDirectories(): Promise { try { await fs.mkdir(this.hooksDir, { recursive: true }) } catch (error) { // Directory might already exist } } // ============================================================================ // HOOK LOADING // ============================================================================ async loadHooks(): Promise { try { const config: HookConfig = JSON.parse( await fs.readFile(this.hooksFile, 'utf-8') ) for (const [event, hookGroup] of Object.entries(config.hooks)) { this.hooks.set(event as HookEvent, hookGroup.hooks) } } catch (error) { // No hooks file or invalid JSON await this.saveHooks() } } async loadPluginHooks(pluginPath: string): Promise { const hooksJsonPath = path.join(pluginPath, 'hooks', 'hooks.json') try { const config: HookConfig = JSON.parse( await fs.readFile(hooksJsonPath, 'utf-8') ) for (const [event, hookGroup] of Object.entries(config.hooks)) { const existing = this.hooks.get(event as HookEvent) || [] this.hooks.set(event as HookEvent, [...existing, ...hookGroup.hooks]) } } catch { // No hooks file } } async saveHooks(): Promise { const config: HookConfig = { description: 'Claude Code Hooks Configuration', hooks: {}, } for (const [event, hooks] of this.hooks.entries()) { config.hooks[event] = { hooks } } await fs.writeFile(this.hooksFile, JSON.stringify(config, null, 2)) } // ============================================================================ // HOOK REGISTRATION // ============================================================================ registerHook(event: HookEvent, hook: HookDefinition): void { const existing = this.hooks.get(event) || [] existing.push(hook) this.hooks.set(event, existing.sort((a, b) => (b.priority || 0) - (a.priority || 0))) } unregisterHook(event: HookEvent, hookIdentifier: string): void { const existing = this.hooks.get(event) || [] const filtered = existing.filter( (h) => h.command !== hookIdentifier && h.module !== hookIdentifier ) this.hooks.set(event, filtered) } clearHooks(event?: HookEvent): void { if (event) { this.hooks.delete(event) } else { this.hooks.clear() } } // ============================================================================ // HOOK EXECUTION // ============================================================================ async executeHook(event: HookEvent, context: HookContext): Promise { const hooks = this.hooks.get(event) || [] const results: HookResult[] = [] for (const hook of hooks) { if (hook.enabled === false) continue try { const result = await this.executeSingleHook(hook, context) results.push(result) // Check if hook wants to cancel the operation if (result.modifications?.cancel) { break } } catch (error) { results.push({ success: false, error: error instanceof Error ? error.message : String(error), }) } } // Store results for later retrieval this.hookResults.set(`${event}-${context.timestamp}`, results) return results } private async executeSingleHook( hook: HookDefinition, context: HookContext ): Promise { const timeout = hook.timeout || 5000 if (hook.type === 'command' && hook.command) { return await this.executeCommandHook(hook.command, context, timeout) } else if (hook.type === 'module' && hook.module) { return await this.executeModuleHook(hook.module, context, timeout) } throw new Error(`Unknown hook type`) } private async executeCommandHook( command: string, context: HookContext, timeout: number ): Promise { return new Promise((resolve, reject) => { const [cmd, ...args] = command.split(' ') const proc = spawn(cmd, args, { env: { ...process.env, HOOK_EVENT: context.event, HOOK_DATA: JSON.stringify(context.data), HOOK_TIMESTAMP: context.timestamp, }, stdio: ['ignore', 'pipe', 'pipe'], }) let stdout = '' let stderr = '' proc.stdout?.on('data', (data) => { stdout += data.toString() }) proc.stderr?.on('data', (data) => { stderr += data.toString() }) const timer = setTimeout(() => { proc.kill() reject(new Error(`Hook timeout after ${timeout}ms`)) }, timeout) proc.on('close', (code) => { clearTimeout(timer) if (code === 0) { try { // Try to parse output as JSON for modifications const modifications = stdout.trim() ? JSON.parse(stdout) : undefined resolve({ success: true, data: stdout, modifications, }) } catch { resolve({ success: true, data: stdout, }) } } else { reject(new Error(`Hook failed: ${stderr || `exit code ${code}`}`)) } }) }) } private async executeModuleHook( modulePath: string, context: HookContext, timeout: number ): Promise { // Dynamic import for TypeScript/JavaScript modules const startTime = Date.now() try { const module = await import(modulePath) const handler = module.default || module.hook || module.handler if (typeof handler !== 'function') { throw new Error(`Module ${modulePath} does not export a handler function`) } // Execute with timeout const result = await Promise.race([ handler(context), new Promise((_, reject) => setTimeout(() => reject(new Error('Hook timeout')), timeout) ), ]) return { success: true, data: result, } } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), } } } // ============================================================================ // UTILITY FUNCTIONS // ============================================================================ getHookResults(event: HookEvent, timestamp: string): HookResult[] | undefined { return this.hookResults.get(`${event}-${timestamp}`) } getRegisteredHooks(event?: HookEvent): Map { if (event) { const hooks = this.hooks.get(event) return new Map(hooks ? [[event, hooks]] : []) } return this.hooks } async listHooksByEvent(event: HookEvent): Promise { return this.hooks.get(event) || [] } } // ============================================================================ // HOOK BUILDER - Convenient API for Creating Hooks // ============================================================================ export class HookBuilder { private hooks: HookDefinition[] = [] command(cmd: string, options?: Partial): this { this.hooks.push({ type: 'command', command: cmd, priority: options?.priority || 0, timeout: options?.timeout || 5000, enabled: options?.enabled !== false, condition: options?.condition, }) return this } module(mod: string, options?: Partial): this { this.hooks.push({ type: 'module', module: mod, priority: options?.priority || 0, timeout: options?.timeout || 5000, enabled: options?.enabled !== false, condition: options?.condition, }) return this } build(): HookDefinition[] { return this.hooks } } // ============================================================================ // HELPER FUNCTIONS // ============================================================================ export function createHookBuilder(): HookBuilder { return new HookBuilder() } export async function executeHooks( hookSystem: HookSystem, event: HookEvent, data: Record ): Promise { const context: HookContext = { event, timestamp: Date.now().toString(), data, } return await hookSystem.executeHook(event, context) } // ============================================================================ // EXPORTS // ============================================================================ export default HookSystem