import { getMainThreadAgentType } from '../bootstrap/state.js' import type { HookResultMessage } from '../types/message.js' import { createAttachmentMessage } from './attachments.js' import { logForDebugging } from './debug.js' import { withDiagnosticsTiming } from './diagLogs.js' import { isBareMode } from './envUtils.js' import { updateWatchPaths } from './hooks/fileChangedWatcher.js' import { shouldAllowManagedHooksOnly } from './hooks/hooksConfigSnapshot.js' import { executeSessionStartHooks, executeSetupHooks } from './hooks.js' import { logError } from './log.js' import { loadPluginHooks } from './plugins/loadPluginHooks.js' type SessionStartHooksOptions = { sessionId?: string agentType?: string model?: string forceSyncExecution?: boolean } // Set by processSessionStartHooks when a hook emits initialUserMessage; // consumed once by takeInitialUserMessage. This side channel avoids changing // the Promise return type that main.tsx and print.ts // both already await on (sessionStartHooksPromise is kicked in main.tsx and // joined later — rippling a structural return-type change through that // handoff would touch five callsites for what is a print-mode-only value). let pendingInitialUserMessage: string | undefined export function takeInitialUserMessage(): string | undefined { const v = pendingInitialUserMessage pendingInitialUserMessage = undefined return v } // Note to CLAUDE: do not add ANY "warmup" logic. It is **CRITICAL** that you do not add extra work on startup. export async function processSessionStartHooks( source: 'startup' | 'resume' | 'clear' | 'compact', { sessionId, agentType, model, forceSyncExecution, }: SessionStartHooksOptions = {}, ): Promise { // --bare skips all hooks. executeHooks already early-returns under --bare // (hooks.ts:1861), but this skips the loadPluginHooks() await below too — // no point loading plugin hooks that'll never run. if (isBareMode()) { return [] } const hookMessages: HookResultMessage[] = [] const additionalContexts: string[] = [] const allWatchPaths: string[] = [] // Skip loading plugin hooks if restricted to managed hooks only // Plugin hooks are untrusted external code that should be blocked by policy if (shouldAllowManagedHooksOnly()) { logForDebugging('Skipping plugin hooks - allowManagedHooksOnly is enabled') } else { // Ensure plugin hooks are loaded before executing SessionStart hooks. // loadPluginHooks() may be called early during startup (fire-and-forget, non-blocking) // to pre-load hooks, but we must guarantee hooks are registered before executing them. // This function is memoized, so if hooks are already loaded, this returns immediately // with negligible overhead (just a cache lookup). try { await withDiagnosticsTiming('load_plugin_hooks', () => loadPluginHooks()) } catch (error) { // Log error but don't crash - continue with session start without plugin hooks /* eslint-disable no-restricted-syntax -- both branches wrap with context, not a toError case */ const enhancedError = error instanceof Error ? new Error( `Failed to load plugin hooks during ${source}: ${error.message}`, ) : new Error( `Failed to load plugin hooks during ${source}: ${String(error)}`, ) /* eslint-enable no-restricted-syntax */ if (error instanceof Error && error.stack) { enhancedError.stack = error.stack } logError(enhancedError) // Provide specific guidance based on error type const errorMessage = error instanceof Error ? error.message : String(error) let userGuidance = '' if ( errorMessage.includes('Failed to clone') || errorMessage.includes('network') || errorMessage.includes('ETIMEDOUT') || errorMessage.includes('ENOTFOUND') ) { userGuidance = 'This appears to be a network issue. Check your internet connection and try again.' } else if ( errorMessage.includes('Permission denied') || errorMessage.includes('EACCES') || errorMessage.includes('EPERM') ) { userGuidance = 'This appears to be a permissions issue. Check file permissions on ~/.claude/plugins/' } else if ( errorMessage.includes('Invalid') || errorMessage.includes('parse') || errorMessage.includes('JSON') || errorMessage.includes('schema') ) { userGuidance = 'This appears to be a configuration issue. Check your plugin settings in .claude/settings.json' } else { userGuidance = 'Please fix the plugin configuration or remove problematic plugins from your settings.' } logForDebugging( `Warning: Failed to load plugin hooks. SessionStart hooks from plugins will not execute. ` + `Error: ${errorMessage}. ${userGuidance}`, { level: 'warn' }, ) // Continue execution - plugin hooks won't be available, but project-level hooks // from .claude/settings.json (loaded via captureHooksConfigSnapshot) will still work } } // Execute SessionStart hooks, ignoring blocking errors // Use the provided agentType or fall back to the one stored in bootstrap state const resolvedAgentType = agentType ?? getMainThreadAgentType() for await (const hookResult of executeSessionStartHooks( source, sessionId, resolvedAgentType, model, undefined, undefined, forceSyncExecution, )) { if (hookResult.message) { hookMessages.push(hookResult.message) } if ( hookResult.additionalContexts && hookResult.additionalContexts.length > 0 ) { additionalContexts.push(...hookResult.additionalContexts) } if (hookResult.initialUserMessage) { pendingInitialUserMessage = hookResult.initialUserMessage } if (hookResult.watchPaths && hookResult.watchPaths.length > 0) { allWatchPaths.push(...hookResult.watchPaths) } } if (allWatchPaths.length > 0) { updateWatchPaths(allWatchPaths) } // If hooks provided additional context, add it as a message if (additionalContexts.length > 0) { const contextMessage = createAttachmentMessage({ type: 'hook_additional_context', content: additionalContexts, hookName: 'SessionStart', toolUseID: 'SessionStart', hookEvent: 'SessionStart', }) hookMessages.push(contextMessage) } return hookMessages } export async function processSetupHooks( trigger: 'init' | 'maintenance', { forceSyncExecution }: { forceSyncExecution?: boolean } = {}, ): Promise { // Same rationale as processSessionStartHooks above. if (isBareMode()) { return [] } const hookMessages: HookResultMessage[] = [] const additionalContexts: string[] = [] if (shouldAllowManagedHooksOnly()) { logForDebugging('Skipping plugin hooks - allowManagedHooksOnly is enabled') } else { try { await loadPluginHooks() } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) logForDebugging( `Warning: Failed to load plugin hooks. Setup hooks from plugins will not execute. Error: ${errorMessage}`, { level: 'warn' }, ) } } for await (const hookResult of executeSetupHooks( trigger, undefined, undefined, forceSyncExecution, )) { if (hookResult.message) { hookMessages.push(hookResult.message) } if ( hookResult.additionalContexts && hookResult.additionalContexts.length > 0 ) { additionalContexts.push(...hookResult.additionalContexts) } } if (additionalContexts.length > 0) { const contextMessage = createAttachmentMessage({ type: 'hook_additional_context', content: additionalContexts, hookName: 'Setup', toolUseID: 'Setup', hookEvent: 'Setup', }) hookMessages.push(contextMessage) } return hookMessages }