source dump of claude code
at main 232 lines 8.2 kB view raw
1import { getMainThreadAgentType } from '../bootstrap/state.js' 2import type { HookResultMessage } from '../types/message.js' 3import { createAttachmentMessage } from './attachments.js' 4import { logForDebugging } from './debug.js' 5import { withDiagnosticsTiming } from './diagLogs.js' 6import { isBareMode } from './envUtils.js' 7import { updateWatchPaths } from './hooks/fileChangedWatcher.js' 8import { shouldAllowManagedHooksOnly } from './hooks/hooksConfigSnapshot.js' 9import { executeSessionStartHooks, executeSetupHooks } from './hooks.js' 10import { logError } from './log.js' 11import { loadPluginHooks } from './plugins/loadPluginHooks.js' 12 13type SessionStartHooksOptions = { 14 sessionId?: string 15 agentType?: string 16 model?: string 17 forceSyncExecution?: boolean 18} 19 20// Set by processSessionStartHooks when a hook emits initialUserMessage; 21// consumed once by takeInitialUserMessage. This side channel avoids changing 22// the Promise<HookResultMessage[]> return type that main.tsx and print.ts 23// both already await on (sessionStartHooksPromise is kicked in main.tsx and 24// joined later — rippling a structural return-type change through that 25// handoff would touch five callsites for what is a print-mode-only value). 26let pendingInitialUserMessage: string | undefined 27 28export function takeInitialUserMessage(): string | undefined { 29 const v = pendingInitialUserMessage 30 pendingInitialUserMessage = undefined 31 return v 32} 33 34// Note to CLAUDE: do not add ANY "warmup" logic. It is **CRITICAL** that you do not add extra work on startup. 35export async function processSessionStartHooks( 36 source: 'startup' | 'resume' | 'clear' | 'compact', 37 { 38 sessionId, 39 agentType, 40 model, 41 forceSyncExecution, 42 }: SessionStartHooksOptions = {}, 43): Promise<HookResultMessage[]> { 44 // --bare skips all hooks. executeHooks already early-returns under --bare 45 // (hooks.ts:1861), but this skips the loadPluginHooks() await below too — 46 // no point loading plugin hooks that'll never run. 47 if (isBareMode()) { 48 return [] 49 } 50 const hookMessages: HookResultMessage[] = [] 51 const additionalContexts: string[] = [] 52 const allWatchPaths: string[] = [] 53 54 // Skip loading plugin hooks if restricted to managed hooks only 55 // Plugin hooks are untrusted external code that should be blocked by policy 56 if (shouldAllowManagedHooksOnly()) { 57 logForDebugging('Skipping plugin hooks - allowManagedHooksOnly is enabled') 58 } else { 59 // Ensure plugin hooks are loaded before executing SessionStart hooks. 60 // loadPluginHooks() may be called early during startup (fire-and-forget, non-blocking) 61 // to pre-load hooks, but we must guarantee hooks are registered before executing them. 62 // This function is memoized, so if hooks are already loaded, this returns immediately 63 // with negligible overhead (just a cache lookup). 64 try { 65 await withDiagnosticsTiming('load_plugin_hooks', () => loadPluginHooks()) 66 } catch (error) { 67 // Log error but don't crash - continue with session start without plugin hooks 68 /* eslint-disable no-restricted-syntax -- both branches wrap with context, not a toError case */ 69 const enhancedError = 70 error instanceof Error 71 ? new Error( 72 `Failed to load plugin hooks during ${source}: ${error.message}`, 73 ) 74 : new Error( 75 `Failed to load plugin hooks during ${source}: ${String(error)}`, 76 ) 77 /* eslint-enable no-restricted-syntax */ 78 79 if (error instanceof Error && error.stack) { 80 enhancedError.stack = error.stack 81 } 82 83 logError(enhancedError) 84 85 // Provide specific guidance based on error type 86 const errorMessage = 87 error instanceof Error ? error.message : String(error) 88 let userGuidance = '' 89 90 if ( 91 errorMessage.includes('Failed to clone') || 92 errorMessage.includes('network') || 93 errorMessage.includes('ETIMEDOUT') || 94 errorMessage.includes('ENOTFOUND') 95 ) { 96 userGuidance = 97 'This appears to be a network issue. Check your internet connection and try again.' 98 } else if ( 99 errorMessage.includes('Permission denied') || 100 errorMessage.includes('EACCES') || 101 errorMessage.includes('EPERM') 102 ) { 103 userGuidance = 104 'This appears to be a permissions issue. Check file permissions on ~/.claude/plugins/' 105 } else if ( 106 errorMessage.includes('Invalid') || 107 errorMessage.includes('parse') || 108 errorMessage.includes('JSON') || 109 errorMessage.includes('schema') 110 ) { 111 userGuidance = 112 'This appears to be a configuration issue. Check your plugin settings in .claude/settings.json' 113 } else { 114 userGuidance = 115 'Please fix the plugin configuration or remove problematic plugins from your settings.' 116 } 117 118 logForDebugging( 119 `Warning: Failed to load plugin hooks. SessionStart hooks from plugins will not execute. ` + 120 `Error: ${errorMessage}. ${userGuidance}`, 121 { level: 'warn' }, 122 ) 123 124 // Continue execution - plugin hooks won't be available, but project-level hooks 125 // from .claude/settings.json (loaded via captureHooksConfigSnapshot) will still work 126 } 127 } 128 129 // Execute SessionStart hooks, ignoring blocking errors 130 // Use the provided agentType or fall back to the one stored in bootstrap state 131 const resolvedAgentType = agentType ?? getMainThreadAgentType() 132 for await (const hookResult of executeSessionStartHooks( 133 source, 134 sessionId, 135 resolvedAgentType, 136 model, 137 undefined, 138 undefined, 139 forceSyncExecution, 140 )) { 141 if (hookResult.message) { 142 hookMessages.push(hookResult.message) 143 } 144 if ( 145 hookResult.additionalContexts && 146 hookResult.additionalContexts.length > 0 147 ) { 148 additionalContexts.push(...hookResult.additionalContexts) 149 } 150 if (hookResult.initialUserMessage) { 151 pendingInitialUserMessage = hookResult.initialUserMessage 152 } 153 if (hookResult.watchPaths && hookResult.watchPaths.length > 0) { 154 allWatchPaths.push(...hookResult.watchPaths) 155 } 156 } 157 158 if (allWatchPaths.length > 0) { 159 updateWatchPaths(allWatchPaths) 160 } 161 162 // If hooks provided additional context, add it as a message 163 if (additionalContexts.length > 0) { 164 const contextMessage = createAttachmentMessage({ 165 type: 'hook_additional_context', 166 content: additionalContexts, 167 hookName: 'SessionStart', 168 toolUseID: 'SessionStart', 169 hookEvent: 'SessionStart', 170 }) 171 hookMessages.push(contextMessage) 172 } 173 174 return hookMessages 175} 176 177export async function processSetupHooks( 178 trigger: 'init' | 'maintenance', 179 { forceSyncExecution }: { forceSyncExecution?: boolean } = {}, 180): Promise<HookResultMessage[]> { 181 // Same rationale as processSessionStartHooks above. 182 if (isBareMode()) { 183 return [] 184 } 185 const hookMessages: HookResultMessage[] = [] 186 const additionalContexts: string[] = [] 187 188 if (shouldAllowManagedHooksOnly()) { 189 logForDebugging('Skipping plugin hooks - allowManagedHooksOnly is enabled') 190 } else { 191 try { 192 await loadPluginHooks() 193 } catch (error) { 194 const errorMessage = 195 error instanceof Error ? error.message : String(error) 196 logForDebugging( 197 `Warning: Failed to load plugin hooks. Setup hooks from plugins will not execute. Error: ${errorMessage}`, 198 { level: 'warn' }, 199 ) 200 } 201 } 202 203 for await (const hookResult of executeSetupHooks( 204 trigger, 205 undefined, 206 undefined, 207 forceSyncExecution, 208 )) { 209 if (hookResult.message) { 210 hookMessages.push(hookResult.message) 211 } 212 if ( 213 hookResult.additionalContexts && 214 hookResult.additionalContexts.length > 0 215 ) { 216 additionalContexts.push(...hookResult.additionalContexts) 217 } 218 } 219 220 if (additionalContexts.length > 0) { 221 const contextMessage = createAttachmentMessage({ 222 type: 'hook_additional_context', 223 content: additionalContexts, 224 hookName: 'Setup', 225 toolUseID: 'Setup', 226 hookEvent: 'Setup', 227 }) 228 hookMessages.push(contextMessage) 229 } 230 231 return hookMessages 232}