233 lines
8.0 KiB
TypeScript
233 lines
8.0 KiB
TypeScript
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<HookResultMessage[]> 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<HookResultMessage[]> {
|
|
// --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<HookResultMessage[]> {
|
|
// 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
|
|
}
|