Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
8.5 kB · 271 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272import { resolve } from 'path'import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js'import { getSessionId } from '../../bootstrap/state.js'import type { AppState } from '../../state/AppState.js'import type { EditableSettingSource } from '../settings/constants.js'import { SOURCES } from '../settings/constants.js'import { getSettingsFilePathForSource, getSettingsForSource,} from '../settings/settings.js'import type { HookCommand, HookMatcher } from '../settings/types.js'import { DEFAULT_HOOK_SHELL } from '../shell/shellProvider.js'import { getSessionHooks } from './sessionHooks.js'
export type HookSource = | EditableSettingSource | 'policySettings' | 'pluginHook' | 'sessionHook' | 'builtinHook'
export interface IndividualHookConfig { event: HookEvent config: HookCommand matcher?: string source: HookSource pluginName?: string}
/** * Check if two hooks are equal (comparing only command/prompt content, not timeout) */export function isHookEqual( a: HookCommand | { type: 'function'; timeout?: number }, b: HookCommand | { type: 'function'; timeout?: number },): boolean { if (a.type !== b.type) return false
// Use switch for exhaustive type checking // Note: We only compare command/prompt content, not timeout // `if` is part of identity: same command with different `if` conditions // are distinct hooks (e.g., setup.sh if=Bash(git *) vs if=Bash(npm *)). const sameIf = (x: { if?: string }, y: { if?: string }) => (x.if ?? '') === (y.if ?? '') switch (a.type) { case 'command': // shell is part of identity: same command string with different // shells are distinct hooks. Default 'bash' so undefined === 'bash'. return ( b.type === 'command' && a.command === b.command && (a.shell ?? DEFAULT_HOOK_SHELL) === (b.shell ?? DEFAULT_HOOK_SHELL) && sameIf(a, b) ) case 'prompt': return b.type === 'prompt' && a.prompt === b.prompt && sameIf(a, b) case 'agent': return b.type === 'agent' && a.prompt === b.prompt && sameIf(a, b) case 'http': return b.type === 'http' && a.url === b.url && sameIf(a, b) case 'function': // Function hooks can't be compared (no stable identifier) return false }}
/** Get the display text for a hook */export function getHookDisplayText( hook: HookCommand | { type: 'callback' | 'function'; statusMessage?: string },): string { // Return custom status message if provided if ('statusMessage' in hook && hook.statusMessage) { return hook.statusMessage }
switch (hook.type) { case 'command': return hook.command case 'prompt': return hook.prompt case 'agent': return hook.prompt case 'http': return hook.url case 'callback': return 'callback' case 'function': return 'function' }}
export function getAllHooks(appState: AppState): IndividualHookConfig[] { const hooks: IndividualHookConfig[] = []
// Check if restricted to managed hooks only const policySettings = getSettingsForSource('policySettings') const restrictedToManagedOnly = policySettings?.allowManagedHooksOnly === true
// If allowManagedHooksOnly is set, don't show any hooks in the UI // (user/project/local are blocked, and managed hooks are intentionally hidden) if (!restrictedToManagedOnly) { // Get hooks from all editable sources const sources = [ 'userSettings', 'projectSettings', 'localSettings', ] as EditableSettingSource[]
// Track which settings files we've already processed to avoid duplicates // (e.g., when running from home directory, userSettings and projectSettings // both resolve to ~/.claude/settings.json) const seenFiles = new Set<string>()
for (const source of sources) { const filePath = getSettingsFilePathForSource(source) if (filePath) { const resolvedPath = resolve(filePath) if (seenFiles.has(resolvedPath)) { continue } seenFiles.add(resolvedPath) }
const sourceSettings = getSettingsForSource(source) if (!sourceSettings?.hooks) { continue }
for (const [event, matchers] of Object.entries(sourceSettings.hooks)) { for (const matcher of matchers as HookMatcher[]) { for (const hookCommand of matcher.hooks) { hooks.push({ event: event as HookEvent, config: hookCommand, matcher: matcher.matcher, source, }) } } } } }
// Get session hooks const sessionId = getSessionId() const sessionHooks = getSessionHooks(appState, sessionId) for (const [event, matchers] of sessionHooks.entries()) { for (const matcher of matchers) { for (const hookCommand of matcher.hooks) { hooks.push({ event, config: hookCommand, matcher: matcher.matcher, source: 'sessionHook', }) } } }
return hooks}
export function getHooksForEvent( appState: AppState, event: HookEvent,): IndividualHookConfig[] { return getAllHooks(appState).filter(hook => hook.event === event)}
export function hookSourceDescriptionDisplayString(source: HookSource): string { switch (source) { case 'userSettings': return 'User settings (~/.claude/settings.json)' case 'projectSettings': return 'Project settings (.claude/settings.json)' case 'localSettings': return 'Local settings (.claude/settings.local.json)' case 'pluginHook': // TODO: Get the actual plugin hook file paths instead of using glob pattern // We should capture the specific plugin paths during hook registration and display them here // e.g., "Plugin hooks (~/.claude/plugins/repos/source/example-plugin/example-plugin/hooks/hooks.json)" return 'Plugin hooks (~/.claude/plugins/*/hooks/hooks.json)' case 'sessionHook': return 'Session hooks (in-memory, temporary)' case 'builtinHook': return 'Built-in hooks (registered internally by Claude Code)' default: return source as string }}
export function hookSourceHeaderDisplayString(source: HookSource): string { switch (source) { case 'userSettings': return 'User Settings' case 'projectSettings': return 'Project Settings' case 'localSettings': return 'Local Settings' case 'pluginHook': return 'Plugin Hooks' case 'sessionHook': return 'Session Hooks' case 'builtinHook': return 'Built-in Hooks' default: return source as string }}
export function hookSourceInlineDisplayString(source: HookSource): string { switch (source) { case 'userSettings': return 'User' case 'projectSettings': return 'Project' case 'localSettings': return 'Local' case 'pluginHook': return 'Plugin' case 'sessionHook': return 'Session' case 'builtinHook': return 'Built-in' default: return source as string }}
export function sortMatchersByPriority( matchers: string[], hooksByEventAndMatcher: Record< string, Record<string, IndividualHookConfig[]> >, selectedEvent: HookEvent,): string[] { // Create a priority map based on SOURCES order (lower index = higher priority) const sourcePriority = SOURCES.reduce( (acc, source, index) => { acc[source] = index return acc }, {} as Record<EditableSettingSource, number>, )
return [...matchers].sort((a, b) => { const aHooks = hooksByEventAndMatcher[selectedEvent]?.[a] || [] const bHooks = hooksByEventAndMatcher[selectedEvent]?.[b] || []
const aSources = Array.from(new Set(aHooks.map(h => h.source))) const bSources = Array.from(new Set(bHooks.map(h => h.source)))
// Sort by highest priority source first (lowest priority number) // Plugin hooks get lowest priority (highest number) const getSourcePriority = (source: HookSource) => source === 'pluginHook' || source === 'builtinHook' ? 999 : sourcePriority[source as EditableSettingSource]
const aHighestPriority = Math.min(...aSources.map(getSourcePriority)) const bHighestPriority = Math.min(...bSources.map(getSourcePriority))
if (aHighestPriority !== bHighestPriority) { return aHighestPriority - bHighestPriority }
// If same priority, sort by matcher name return a.localeCompare(b) })}