source dump of claude code
1// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
2import { z } from 'zod/v4'
3import { lazySchema } from '../utils/lazySchema.js'
4import {
5 type HookEvent,
6 HOOK_EVENTS,
7 type HookInput,
8 type PermissionUpdate,
9} from 'src/entrypoints/agentSdkTypes.js'
10import type {
11 HookJSONOutput,
12 AsyncHookJSONOutput,
13 SyncHookJSONOutput,
14} from 'src/entrypoints/agentSdkTypes.js'
15import type { Message } from 'src/types/message.js'
16import type { PermissionResult } from 'src/utils/permissions/PermissionResult.js'
17import { permissionBehaviorSchema } from 'src/utils/permissions/PermissionRule.js'
18import { permissionUpdateSchema } from 'src/utils/permissions/PermissionUpdateSchema.js'
19import type { AppState } from '../state/AppState.js'
20import type { AttributionState } from '../utils/commitAttribution.js'
21
22export function isHookEvent(value: string): value is HookEvent {
23 return HOOK_EVENTS.includes(value as HookEvent)
24}
25
26// Prompt elicitation protocol types. The `prompt` key acts as discriminator
27// (mirroring the {async:true} pattern), with the id as its value.
28export const promptRequestSchema = lazySchema(() =>
29 z.object({
30 prompt: z.string(), // request id
31 message: z.string(),
32 options: z.array(
33 z.object({
34 key: z.string(),
35 label: z.string(),
36 description: z.string().optional(),
37 }),
38 ),
39 }),
40)
41
42export type PromptRequest = z.infer<ReturnType<typeof promptRequestSchema>>
43
44export type PromptResponse = {
45 prompt_response: string // request id
46 selected: string
47}
48
49// Sync hook response schema
50export const syncHookResponseSchema = lazySchema(() =>
51 z.object({
52 continue: z
53 .boolean()
54 .describe('Whether Claude should continue after hook (default: true)')
55 .optional(),
56 suppressOutput: z
57 .boolean()
58 .describe('Hide stdout from transcript (default: false)')
59 .optional(),
60 stopReason: z
61 .string()
62 .describe('Message shown when continue is false')
63 .optional(),
64 decision: z.enum(['approve', 'block']).optional(),
65 reason: z.string().describe('Explanation for the decision').optional(),
66 systemMessage: z
67 .string()
68 .describe('Warning message shown to the user')
69 .optional(),
70 hookSpecificOutput: z
71 .union([
72 z.object({
73 hookEventName: z.literal('PreToolUse'),
74 permissionDecision: permissionBehaviorSchema().optional(),
75 permissionDecisionReason: z.string().optional(),
76 updatedInput: z.record(z.string(), z.unknown()).optional(),
77 additionalContext: z.string().optional(),
78 }),
79 z.object({
80 hookEventName: z.literal('UserPromptSubmit'),
81 additionalContext: z.string().optional(),
82 }),
83 z.object({
84 hookEventName: z.literal('SessionStart'),
85 additionalContext: z.string().optional(),
86 initialUserMessage: z.string().optional(),
87 watchPaths: z
88 .array(z.string())
89 .describe('Absolute paths to watch for FileChanged hooks')
90 .optional(),
91 }),
92 z.object({
93 hookEventName: z.literal('Setup'),
94 additionalContext: z.string().optional(),
95 }),
96 z.object({
97 hookEventName: z.literal('SubagentStart'),
98 additionalContext: z.string().optional(),
99 }),
100 z.object({
101 hookEventName: z.literal('PostToolUse'),
102 additionalContext: z.string().optional(),
103 updatedMCPToolOutput: z
104 .unknown()
105 .describe('Updates the output for MCP tools')
106 .optional(),
107 }),
108 z.object({
109 hookEventName: z.literal('PostToolUseFailure'),
110 additionalContext: z.string().optional(),
111 }),
112 z.object({
113 hookEventName: z.literal('PermissionDenied'),
114 retry: z.boolean().optional(),
115 }),
116 z.object({
117 hookEventName: z.literal('Notification'),
118 additionalContext: z.string().optional(),
119 }),
120 z.object({
121 hookEventName: z.literal('PermissionRequest'),
122 decision: z.union([
123 z.object({
124 behavior: z.literal('allow'),
125 updatedInput: z.record(z.string(), z.unknown()).optional(),
126 updatedPermissions: z.array(permissionUpdateSchema()).optional(),
127 }),
128 z.object({
129 behavior: z.literal('deny'),
130 message: z.string().optional(),
131 interrupt: z.boolean().optional(),
132 }),
133 ]),
134 }),
135 z.object({
136 hookEventName: z.literal('Elicitation'),
137 action: z.enum(['accept', 'decline', 'cancel']).optional(),
138 content: z.record(z.string(), z.unknown()).optional(),
139 }),
140 z.object({
141 hookEventName: z.literal('ElicitationResult'),
142 action: z.enum(['accept', 'decline', 'cancel']).optional(),
143 content: z.record(z.string(), z.unknown()).optional(),
144 }),
145 z.object({
146 hookEventName: z.literal('CwdChanged'),
147 watchPaths: z
148 .array(z.string())
149 .describe('Absolute paths to watch for FileChanged hooks')
150 .optional(),
151 }),
152 z.object({
153 hookEventName: z.literal('FileChanged'),
154 watchPaths: z
155 .array(z.string())
156 .describe('Absolute paths to watch for FileChanged hooks')
157 .optional(),
158 }),
159 z.object({
160 hookEventName: z.literal('WorktreeCreate'),
161 worktreePath: z.string(),
162 }),
163 ])
164 .optional(),
165 }),
166)
167
168// Zod schema for hook JSON output validation
169export const hookJSONOutputSchema = lazySchema(() => {
170 // Async hook response schema
171 const asyncHookResponseSchema = z.object({
172 async: z.literal(true),
173 asyncTimeout: z.number().optional(),
174 })
175 return z.union([asyncHookResponseSchema, syncHookResponseSchema()])
176})
177
178// Infer the TypeScript type from the schema
179type SchemaHookJSONOutput = z.infer<ReturnType<typeof hookJSONOutputSchema>>
180
181// Type guard function to check if response is sync
182export function isSyncHookJSONOutput(
183 json: HookJSONOutput,
184): json is SyncHookJSONOutput {
185 return !('async' in json && json.async === true)
186}
187
188// Type guard function to check if response is async
189export function isAsyncHookJSONOutput(
190 json: HookJSONOutput,
191): json is AsyncHookJSONOutput {
192 return 'async' in json && json.async === true
193}
194
195// Compile-time assertion that SDK and Zod types match
196import type { IsEqual } from 'type-fest'
197type Assert<T extends true> = T
198type _assertSDKTypesMatch = Assert<
199 IsEqual<SchemaHookJSONOutput, HookJSONOutput>
200>
201
202/** Context passed to callback hooks for state access */
203export type HookCallbackContext = {
204 getAppState: () => AppState
205 updateAttributionState: (
206 updater: (prev: AttributionState) => AttributionState,
207 ) => void
208}
209
210/** Hook that is a callback. */
211export type HookCallback = {
212 type: 'callback'
213 callback: (
214 input: HookInput,
215 toolUseID: string | null,
216 abort: AbortSignal | undefined,
217 /** Hook index for SessionStart hooks to compute CLAUDE_ENV_FILE path */
218 hookIndex?: number,
219 /** Optional context for accessing app state */
220 context?: HookCallbackContext,
221 ) => Promise<HookJSONOutput>
222 /** Timeout in seconds for this hook */
223 timeout?: number
224 /** Internal hooks (e.g. session file access analytics) are excluded from tengu_run_hook metrics */
225 internal?: boolean
226}
227
228export type HookCallbackMatcher = {
229 matcher?: string
230 hooks: HookCallback[]
231 pluginName?: string
232}
233
234export type HookProgress = {
235 type: 'hook_progress'
236 hookEvent: HookEvent
237 hookName: string
238 command: string
239 promptText?: string
240 statusMessage?: string
241}
242
243export type HookBlockingError = {
244 blockingError: string
245 command: string
246}
247
248export type PermissionRequestResult =
249 | {
250 behavior: 'allow'
251 updatedInput?: Record<string, unknown>
252 updatedPermissions?: PermissionUpdate[]
253 }
254 | {
255 behavior: 'deny'
256 message?: string
257 interrupt?: boolean
258 }
259
260export type HookResult = {
261 message?: Message
262 systemMessage?: Message
263 blockingError?: HookBlockingError
264 outcome: 'success' | 'blocking' | 'non_blocking_error' | 'cancelled'
265 preventContinuation?: boolean
266 stopReason?: string
267 permissionBehavior?: 'ask' | 'deny' | 'allow' | 'passthrough'
268 hookPermissionDecisionReason?: string
269 additionalContext?: string
270 initialUserMessage?: string
271 updatedInput?: Record<string, unknown>
272 updatedMCPToolOutput?: unknown
273 permissionRequestResult?: PermissionRequestResult
274 retry?: boolean
275}
276
277export type AggregatedHookResult = {
278 message?: Message
279 blockingErrors?: HookBlockingError[]
280 preventContinuation?: boolean
281 stopReason?: string
282 hookPermissionDecisionReason?: string
283 permissionBehavior?: PermissionResult['behavior']
284 additionalContexts?: string[]
285 initialUserMessage?: string
286 updatedInput?: Record<string, unknown>
287 updatedMCPToolOutput?: unknown
288 permissionRequestResult?: PermissionRequestResult
289 retry?: boolean
290}