// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered import { z } from 'zod/v4' import { lazySchema } from '../utils/lazySchema.js' import { type HookEvent, HOOK_EVENTS, type HookInput, type PermissionUpdate, } from 'src/entrypoints/agentSdkTypes.js' import type { HookJSONOutput, AsyncHookJSONOutput, SyncHookJSONOutput, } from 'src/entrypoints/agentSdkTypes.js' import type { Message } from 'src/types/message.js' import type { PermissionResult } from 'src/utils/permissions/PermissionResult.js' import { permissionBehaviorSchema } from 'src/utils/permissions/PermissionRule.js' import { permissionUpdateSchema } from 'src/utils/permissions/PermissionUpdateSchema.js' import type { AppState } from '../state/AppState.js' import type { AttributionState } from '../utils/commitAttribution.js' export function isHookEvent(value: string): value is HookEvent { return HOOK_EVENTS.includes(value as HookEvent) } // Prompt elicitation protocol types. The `prompt` key acts as discriminator // (mirroring the {async:true} pattern), with the id as its value. export const promptRequestSchema = lazySchema(() => z.object({ prompt: z.string(), // request id message: z.string(), options: z.array( z.object({ key: z.string(), label: z.string(), description: z.string().optional(), }), ), }), ) export type PromptRequest = z.infer> export type PromptResponse = { prompt_response: string // request id selected: string } // Sync hook response schema export const syncHookResponseSchema = lazySchema(() => z.object({ continue: z .boolean() .describe('Whether Claude should continue after hook (default: true)') .optional(), suppressOutput: z .boolean() .describe('Hide stdout from transcript (default: false)') .optional(), stopReason: z .string() .describe('Message shown when continue is false') .optional(), decision: z.enum(['approve', 'block']).optional(), reason: z.string().describe('Explanation for the decision').optional(), systemMessage: z .string() .describe('Warning message shown to the user') .optional(), hookSpecificOutput: z .union([ z.object({ hookEventName: z.literal('PreToolUse'), permissionDecision: permissionBehaviorSchema().optional(), permissionDecisionReason: z.string().optional(), updatedInput: z.record(z.string(), z.unknown()).optional(), additionalContext: z.string().optional(), }), z.object({ hookEventName: z.literal('UserPromptSubmit'), additionalContext: z.string().optional(), }), z.object({ hookEventName: z.literal('SessionStart'), additionalContext: z.string().optional(), initialUserMessage: z.string().optional(), watchPaths: z .array(z.string()) .describe('Absolute paths to watch for FileChanged hooks') .optional(), }), z.object({ hookEventName: z.literal('Setup'), additionalContext: z.string().optional(), }), z.object({ hookEventName: z.literal('SubagentStart'), additionalContext: z.string().optional(), }), z.object({ hookEventName: z.literal('PostToolUse'), additionalContext: z.string().optional(), updatedMCPToolOutput: z .unknown() .describe('Updates the output for MCP tools') .optional(), }), z.object({ hookEventName: z.literal('PostToolUseFailure'), additionalContext: z.string().optional(), }), z.object({ hookEventName: z.literal('PermissionDenied'), retry: z.boolean().optional(), }), z.object({ hookEventName: z.literal('Notification'), additionalContext: z.string().optional(), }), z.object({ hookEventName: z.literal('PermissionRequest'), decision: z.union([ z.object({ behavior: z.literal('allow'), updatedInput: z.record(z.string(), z.unknown()).optional(), updatedPermissions: z.array(permissionUpdateSchema()).optional(), }), z.object({ behavior: z.literal('deny'), message: z.string().optional(), interrupt: z.boolean().optional(), }), ]), }), z.object({ hookEventName: z.literal('Elicitation'), action: z.enum(['accept', 'decline', 'cancel']).optional(), content: z.record(z.string(), z.unknown()).optional(), }), z.object({ hookEventName: z.literal('ElicitationResult'), action: z.enum(['accept', 'decline', 'cancel']).optional(), content: z.record(z.string(), z.unknown()).optional(), }), z.object({ hookEventName: z.literal('CwdChanged'), watchPaths: z .array(z.string()) .describe('Absolute paths to watch for FileChanged hooks') .optional(), }), z.object({ hookEventName: z.literal('FileChanged'), watchPaths: z .array(z.string()) .describe('Absolute paths to watch for FileChanged hooks') .optional(), }), z.object({ hookEventName: z.literal('WorktreeCreate'), worktreePath: z.string(), }), ]) .optional(), }), ) // Zod schema for hook JSON output validation export const hookJSONOutputSchema = lazySchema(() => { // Async hook response schema const asyncHookResponseSchema = z.object({ async: z.literal(true), asyncTimeout: z.number().optional(), }) return z.union([asyncHookResponseSchema, syncHookResponseSchema()]) }) // Infer the TypeScript type from the schema type SchemaHookJSONOutput = z.infer> // Type guard function to check if response is sync export function isSyncHookJSONOutput( json: HookJSONOutput, ): json is SyncHookJSONOutput { return !('async' in json && json.async === true) } // Type guard function to check if response is async export function isAsyncHookJSONOutput( json: HookJSONOutput, ): json is AsyncHookJSONOutput { return 'async' in json && json.async === true } // Compile-time assertion that SDK and Zod types match import type { IsEqual } from 'type-fest' type Assert = T type _assertSDKTypesMatch = Assert< IsEqual > /** Context passed to callback hooks for state access */ export type HookCallbackContext = { getAppState: () => AppState updateAttributionState: ( updater: (prev: AttributionState) => AttributionState, ) => void } /** Hook that is a callback. */ export type HookCallback = { type: 'callback' callback: ( input: HookInput, toolUseID: string | null, abort: AbortSignal | undefined, /** Hook index for SessionStart hooks to compute CLAUDE_ENV_FILE path */ hookIndex?: number, /** Optional context for accessing app state */ context?: HookCallbackContext, ) => Promise /** Timeout in seconds for this hook */ timeout?: number /** Internal hooks (e.g. session file access analytics) are excluded from tengu_run_hook metrics */ internal?: boolean } export type HookCallbackMatcher = { matcher?: string hooks: HookCallback[] pluginName?: string } export type HookProgress = { type: 'hook_progress' hookEvent: HookEvent hookName: string command: string promptText?: string statusMessage?: string } export type HookBlockingError = { blockingError: string command: string } export type PermissionRequestResult = | { behavior: 'allow' updatedInput?: Record updatedPermissions?: PermissionUpdate[] } | { behavior: 'deny' message?: string interrupt?: boolean } export type HookResult = { message?: Message systemMessage?: Message blockingError?: HookBlockingError outcome: 'success' | 'blocking' | 'non_blocking_error' | 'cancelled' preventContinuation?: boolean stopReason?: string permissionBehavior?: 'ask' | 'deny' | 'allow' | 'passthrough' hookPermissionDecisionReason?: string additionalContext?: string initialUserMessage?: string updatedInput?: Record updatedMCPToolOutput?: unknown permissionRequestResult?: PermissionRequestResult retry?: boolean } export type AggregatedHookResult = { message?: Message blockingErrors?: HookBlockingError[] preventContinuation?: boolean stopReason?: string hookPermissionDecisionReason?: string permissionBehavior?: PermissionResult['behavior'] additionalContexts?: string[] initialUserMessage?: string updatedInput?: Record updatedMCPToolOutput?: unknown permissionRequestResult?: PermissionRequestResult retry?: boolean }