source dump of claude code
at main 211 lines 6.8 kB view raw
1import { randomUUID } from 'crypto' 2import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js' 3import { queryModelWithoutStreaming } from '../../services/api/claude.js' 4import type { ToolUseContext } from '../../Tool.js' 5import type { Message } from '../../types/message.js' 6import { createAttachmentMessage } from '../attachments.js' 7import { createCombinedAbortSignal } from '../combinedAbortSignal.js' 8import { logForDebugging } from '../debug.js' 9import { errorMessage } from '../errors.js' 10import type { HookResult } from '../hooks.js' 11import { safeParseJSON } from '../json.js' 12import { createUserMessage, extractTextContent } from '../messages.js' 13import { getSmallFastModel } from '../model/model.js' 14import type { PromptHook } from '../settings/types.js' 15import { asSystemPrompt } from '../systemPromptType.js' 16import { addArgumentsToPrompt, hookResponseSchema } from './hookHelpers.js' 17 18/** 19 * Execute a prompt-based hook using an LLM 20 */ 21export async function execPromptHook( 22 hook: PromptHook, 23 hookName: string, 24 hookEvent: HookEvent, 25 jsonInput: string, 26 signal: AbortSignal, 27 toolUseContext: ToolUseContext, 28 messages?: Message[], 29 toolUseID?: string, 30): Promise<HookResult> { 31 // Use provided toolUseID or generate a new one 32 const effectiveToolUseID = toolUseID || `hook-${randomUUID()}` 33 try { 34 // Replace $ARGUMENTS with the JSON input 35 const processedPrompt = addArgumentsToPrompt(hook.prompt, jsonInput) 36 logForDebugging( 37 `Hooks: Processing prompt hook with prompt: ${processedPrompt}`, 38 ) 39 40 // Create user message directly - no need for processUserInput which would 41 // trigger UserPromptSubmit hooks and cause infinite recursion 42 const userMessage = createUserMessage({ content: processedPrompt }) 43 44 // Prepend conversation history if provided 45 const messagesToQuery = 46 messages && messages.length > 0 47 ? [...messages, userMessage] 48 : [userMessage] 49 50 logForDebugging( 51 `Hooks: Querying model with ${messagesToQuery.length} messages`, 52 ) 53 54 // Query the model with Haiku 55 const hookTimeoutMs = hook.timeout ? hook.timeout * 1000 : 30000 56 57 // Combined signal: aborts if either the hook signal or timeout triggers 58 const { signal: combinedSignal, cleanup: cleanupSignal } = 59 createCombinedAbortSignal(signal, { timeoutMs: hookTimeoutMs }) 60 61 try { 62 const response = await queryModelWithoutStreaming({ 63 messages: messagesToQuery, 64 systemPrompt: asSystemPrompt([ 65 `You are evaluating a hook in Claude Code. 66 67Your response must be a JSON object matching one of the following schemas: 681. If the condition is met, return: {"ok": true} 692. If the condition is not met, return: {"ok": false, "reason": "Reason for why it is not met"}`, 70 ]), 71 thinkingConfig: { type: 'disabled' as const }, 72 tools: toolUseContext.options.tools, 73 signal: combinedSignal, 74 options: { 75 async getToolPermissionContext() { 76 const appState = toolUseContext.getAppState() 77 return appState.toolPermissionContext 78 }, 79 model: hook.model ?? getSmallFastModel(), 80 toolChoice: undefined, 81 isNonInteractiveSession: true, 82 hasAppendSystemPrompt: false, 83 agents: [], 84 querySource: 'hook_prompt', 85 mcpTools: [], 86 agentId: toolUseContext.agentId, 87 outputFormat: { 88 type: 'json_schema', 89 schema: { 90 type: 'object', 91 properties: { 92 ok: { type: 'boolean' }, 93 reason: { type: 'string' }, 94 }, 95 required: ['ok'], 96 additionalProperties: false, 97 }, 98 }, 99 }, 100 }) 101 102 cleanupSignal() 103 104 // Extract text content from response 105 const content = extractTextContent(response.message.content) 106 107 // Update response length for spinner display 108 toolUseContext.setResponseLength(length => length + content.length) 109 110 const fullResponse = content.trim() 111 logForDebugging(`Hooks: Model response: ${fullResponse}`) 112 113 const json = safeParseJSON(fullResponse) 114 if (!json) { 115 logForDebugging( 116 `Hooks: error parsing response as JSON: ${fullResponse}`, 117 ) 118 return { 119 hook, 120 outcome: 'non_blocking_error', 121 message: createAttachmentMessage({ 122 type: 'hook_non_blocking_error', 123 hookName, 124 toolUseID: effectiveToolUseID, 125 hookEvent, 126 stderr: 'JSON validation failed', 127 stdout: fullResponse, 128 exitCode: 1, 129 }), 130 } 131 } 132 133 const parsed = hookResponseSchema().safeParse(json) 134 if (!parsed.success) { 135 logForDebugging( 136 `Hooks: model response does not conform to expected schema: ${parsed.error.message}`, 137 ) 138 return { 139 hook, 140 outcome: 'non_blocking_error', 141 message: createAttachmentMessage({ 142 type: 'hook_non_blocking_error', 143 hookName, 144 toolUseID: effectiveToolUseID, 145 hookEvent, 146 stderr: `Schema validation failed: ${parsed.error.message}`, 147 stdout: fullResponse, 148 exitCode: 1, 149 }), 150 } 151 } 152 153 // Failed to meet condition 154 if (!parsed.data.ok) { 155 logForDebugging( 156 `Hooks: Prompt hook condition was not met: ${parsed.data.reason}`, 157 ) 158 return { 159 hook, 160 outcome: 'blocking', 161 blockingError: { 162 blockingError: `Prompt hook condition was not met: ${parsed.data.reason}`, 163 command: hook.prompt, 164 }, 165 preventContinuation: true, 166 stopReason: parsed.data.reason, 167 } 168 } 169 170 // Condition was met 171 logForDebugging(`Hooks: Prompt hook condition was met`) 172 return { 173 hook, 174 outcome: 'success', 175 message: createAttachmentMessage({ 176 type: 'hook_success', 177 hookName, 178 toolUseID: effectiveToolUseID, 179 hookEvent, 180 content: '', 181 }), 182 } 183 } catch (error) { 184 cleanupSignal() 185 186 if (combinedSignal.aborted) { 187 return { 188 hook, 189 outcome: 'cancelled', 190 } 191 } 192 throw error 193 } 194 } catch (error) { 195 const errorMsg = errorMessage(error) 196 logForDebugging(`Hooks: Prompt hook error: ${errorMsg}`) 197 return { 198 hook, 199 outcome: 'non_blocking_error', 200 message: createAttachmentMessage({ 201 type: 'hook_non_blocking_error', 202 hookName, 203 toolUseID: effectiveToolUseID, 204 hookEvent, 205 stderr: `Error executing prompt hook: ${errorMsg}`, 206 stdout: '', 207 exitCode: 1, 208 }), 209 } 210 } 211}