| import { randomUUID } from 'crypto' |
| import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js' |
| import { queryModelWithoutStreaming } from '../../services/api/claude.js' |
| import type { ToolUseContext } from '../../Tool.js' |
| import type { Message } from '../../types/message.js' |
| import { createAttachmentMessage } from '../attachments.js' |
| import { createCombinedAbortSignal } from '../combinedAbortSignal.js' |
| import { logForDebugging } from '../debug.js' |
| import { errorMessage } from '../errors.js' |
| import type { HookResult } from '../hooks.js' |
| import { safeParseJSON } from '../json.js' |
| import { createUserMessage, extractTextContent } from '../messages.js' |
| import { getSmallFastModel } from '../model/model.js' |
| import type { PromptHook } from '../settings/types.js' |
| import { asSystemPrompt } from '../systemPromptType.js' |
| import { addArgumentsToPrompt, hookResponseSchema } from './hookHelpers.js' |
|
|
| |
| |
| |
| export async function execPromptHook( |
| hook: PromptHook, |
| hookName: string, |
| hookEvent: HookEvent, |
| jsonInput: string, |
| signal: AbortSignal, |
| toolUseContext: ToolUseContext, |
| messages?: Message[], |
| toolUseID?: string, |
| ): Promise<HookResult> { |
| |
| const effectiveToolUseID = toolUseID || `hook-${randomUUID()}` |
| try { |
| |
| const processedPrompt = addArgumentsToPrompt(hook.prompt, jsonInput) |
| logForDebugging( |
| `Hooks: Processing prompt hook with prompt: ${processedPrompt}`, |
| ) |
|
|
| |
| |
| const userMessage = createUserMessage({ content: processedPrompt }) |
|
|
| |
| const messagesToQuery = |
| messages && messages.length > 0 |
| ? [...messages, userMessage] |
| : [userMessage] |
|
|
| logForDebugging( |
| `Hooks: Querying model with ${messagesToQuery.length} messages`, |
| ) |
|
|
| |
| const hookTimeoutMs = hook.timeout ? hook.timeout * 1000 : 30000 |
|
|
| |
| const { signal: combinedSignal, cleanup: cleanupSignal } = |
| createCombinedAbortSignal(signal, { timeoutMs: hookTimeoutMs }) |
|
|
| try { |
| const response = await queryModelWithoutStreaming({ |
| messages: messagesToQuery, |
| systemPrompt: asSystemPrompt([ |
| `You are evaluating a hook in Claude Code. |
| |
| Your response must be a JSON object matching one of the following schemas: |
| 1. If the condition is met, return: {"ok": true} |
| 2. If the condition is not met, return: {"ok": false, "reason": "Reason for why it is not met"}`, |
| ]), |
| thinkingConfig: { type: 'disabled' as const }, |
| tools: toolUseContext.options.tools, |
| signal: combinedSignal, |
| options: { |
| async getToolPermissionContext() { |
| const appState = toolUseContext.getAppState() |
| return appState.toolPermissionContext |
| }, |
| model: hook.model ?? getSmallFastModel(), |
| toolChoice: undefined, |
| isNonInteractiveSession: true, |
| hasAppendSystemPrompt: false, |
| agents: [], |
| querySource: 'hook_prompt', |
| mcpTools: [], |
| agentId: toolUseContext.agentId, |
| outputFormat: { |
| type: 'json_schema', |
| schema: { |
| type: 'object', |
| properties: { |
| ok: { type: 'boolean' }, |
| reason: { type: 'string' }, |
| }, |
| required: ['ok'], |
| additionalProperties: false, |
| }, |
| }, |
| }, |
| }) |
|
|
| cleanupSignal() |
|
|
| |
| const content = extractTextContent(response.message.content) |
|
|
| |
| toolUseContext.setResponseLength(length => length + content.length) |
|
|
| const fullResponse = content.trim() |
| logForDebugging(`Hooks: Model response: ${fullResponse}`) |
|
|
| const json = safeParseJSON(fullResponse) |
| if (!json) { |
| logForDebugging( |
| `Hooks: error parsing response as JSON: ${fullResponse}`, |
| ) |
| return { |
| hook, |
| outcome: 'non_blocking_error', |
| message: createAttachmentMessage({ |
| type: 'hook_non_blocking_error', |
| hookName, |
| toolUseID: effectiveToolUseID, |
| hookEvent, |
| stderr: 'JSON validation failed', |
| stdout: fullResponse, |
| exitCode: 1, |
| }), |
| } |
| } |
|
|
| const parsed = hookResponseSchema().safeParse(json) |
| if (!parsed.success) { |
| logForDebugging( |
| `Hooks: model response does not conform to expected schema: ${parsed.error.message}`, |
| ) |
| return { |
| hook, |
| outcome: 'non_blocking_error', |
| message: createAttachmentMessage({ |
| type: 'hook_non_blocking_error', |
| hookName, |
| toolUseID: effectiveToolUseID, |
| hookEvent, |
| stderr: `Schema validation failed: ${parsed.error.message}`, |
| stdout: fullResponse, |
| exitCode: 1, |
| }), |
| } |
| } |
|
|
| |
| if (!parsed.data.ok) { |
| logForDebugging( |
| `Hooks: Prompt hook condition was not met: ${parsed.data.reason}`, |
| ) |
| return { |
| hook, |
| outcome: 'blocking', |
| blockingError: { |
| blockingError: `Prompt hook condition was not met: ${parsed.data.reason}`, |
| command: hook.prompt, |
| }, |
| preventContinuation: true, |
| stopReason: parsed.data.reason, |
| } |
| } |
|
|
| |
| logForDebugging(`Hooks: Prompt hook condition was met`) |
| return { |
| hook, |
| outcome: 'success', |
| message: createAttachmentMessage({ |
| type: 'hook_success', |
| hookName, |
| toolUseID: effectiveToolUseID, |
| hookEvent, |
| content: '', |
| }), |
| } |
| } catch (error) { |
| cleanupSignal() |
|
|
| if (combinedSignal.aborted) { |
| return { |
| hook, |
| outcome: 'cancelled', |
| } |
| } |
| throw error |
| } |
| } catch (error) { |
| const errorMsg = errorMessage(error) |
| logForDebugging(`Hooks: Prompt hook error: ${errorMsg}`) |
| return { |
| hook, |
| outcome: 'non_blocking_error', |
| message: createAttachmentMessage({ |
| type: 'hook_non_blocking_error', |
| hookName, |
| toolUseID: effectiveToolUseID, |
| hookEvent, |
| stderr: `Error executing prompt hook: ${errorMsg}`, |
| stdout: '', |
| exitCode: 1, |
| }), |
| } |
| } |
| } |
|
|