import { homedir } from "os" import { join } from "path" import { existsSync } from "fs" import type { ClaudeHooksConfig, HookMatcher, HookCommand } from "./types" interface RawHookMatcher { matcher?: string pattern?: string hooks: HookCommand[] } interface RawClaudeHooksConfig { PreToolUse?: RawHookMatcher[] PostToolUse?: RawHookMatcher[] UserPromptSubmit?: RawHookMatcher[] Stop?: RawHookMatcher[] PreCompact?: RawHookMatcher[] } function normalizeHookMatcher(raw: RawHookMatcher): HookMatcher { return { matcher: raw.matcher ?? raw.pattern ?? "*", hooks: raw.hooks, } } function normalizeHooksConfig(raw: RawClaudeHooksConfig): ClaudeHooksConfig { const result: ClaudeHooksConfig = {} const eventTypes: (keyof RawClaudeHooksConfig)[] = [ "PreToolUse", "PostToolUse", "UserPromptSubmit", "Stop", "PreCompact", ] for (const eventType of eventTypes) { if (raw[eventType]) { result[eventType] = raw[eventType].map(normalizeHookMatcher) } } return result } export function getClaudeSettingsPaths(customPath?: string): string[] { const home = homedir() const paths = [ join(home, ".claude", "settings.json"), join(process.cwd(), ".claude", "settings.json"), join(process.cwd(), ".claude", "settings.local.json"), ] if (customPath && existsSync(customPath)) { paths.unshift(customPath) } return paths } function mergeHooksConfig( base: ClaudeHooksConfig, override: ClaudeHooksConfig ): ClaudeHooksConfig { const result: ClaudeHooksConfig = { ...base } const eventTypes: (keyof ClaudeHooksConfig)[] = [ "PreToolUse", "PostToolUse", "UserPromptSubmit", "Stop", "PreCompact", ] for (const eventType of eventTypes) { if (override[eventType]) { result[eventType] = [...(base[eventType] || []), ...override[eventType]] } } return result } export async function loadClaudeHooksConfig( customSettingsPath?: string ): Promise { const paths = getClaudeSettingsPaths(customSettingsPath) let mergedConfig: ClaudeHooksConfig = {} for (const settingsPath of paths) { if (existsSync(settingsPath)) { try { const content = await Bun.file(settingsPath).text() const settings = JSON.parse(content) as { hooks?: RawClaudeHooksConfig } if (settings.hooks) { const normalizedHooks = normalizeHooksConfig(settings.hooks) mergedConfig = mergeHooksConfig(mergedConfig, normalizedHooks) } } catch { continue } } } return Object.keys(mergedConfig).length > 0 ? mergedConfig : null }