Files
SuperCharged-Claude-Code-Up…/skills/plugins/core/hook-system.ts
admin b723e2bd7d Reorganize: Move all skills to skills/ folder
- Created skills/ directory
- Moved 272 skills to skills/ subfolder
- Kept agents/ at root level
- Kept installation scripts and docs at root level

Repository structure:
- skills/           - All 272 skills from skills.sh
- agents/           - Agent definitions
- *.sh, *.ps1       - Installation scripts
- README.md, etc.   - Documentation

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-23 18:05:17 +00:00

404 lines
11 KiB
TypeScript

/**
* 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<string, any>
}
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<string, { hooks: HookDefinition[] }>
}
// ============================================================================
// HOOK SYSTEM CLASS
// ============================================================================
export class HookSystem {
private hooksFile: string
private hooksDir: string
private hooks: Map<HookEvent, HookDefinition[]> = new Map()
private hookResults: Map<string, HookResult[]> = 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<void> {
await this.ensureDirectories()
await this.loadHooks()
}
private async ensureDirectories(): Promise<void> {
try {
await fs.mkdir(this.hooksDir, { recursive: true })
} catch (error) {
// Directory might already exist
}
}
// ============================================================================
// HOOK LOADING
// ============================================================================
async loadHooks(): Promise<void> {
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<void> {
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<void> {
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<HookResult[]> {
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<HookResult> {
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<HookResult> {
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<HookResult> {
// 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<any>((_, 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<HookEvent, HookDefinition[]> {
if (event) {
const hooks = this.hooks.get(event)
return new Map(hooks ? [[event, hooks]] : [])
}
return this.hooks
}
async listHooksByEvent(event: HookEvent): Promise<HookDefinition[]> {
return this.hooks.get(event) || []
}
}
// ============================================================================
// HOOK BUILDER - Convenient API for Creating Hooks
// ============================================================================
export class HookBuilder {
private hooks: HookDefinition[] = []
command(cmd: string, options?: Partial<HookDefinition>): 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<HookDefinition>): 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<string, any>
): Promise<HookResult[]> {
const context: HookContext = {
event,
timestamp: Date.now().toString(),
data,
}
return await hookSystem.executeHook(event, context)
}
// ============================================================================
// EXPORTS
// ============================================================================
export default HookSystem