source dump of claude code
at main 222 lines 7.9 kB view raw
1/** 2 * Hook Zod schemas extracted to break import cycles. 3 * 4 * This file contains hook-related schema definitions that were originally 5 * in src/utils/settings/types.ts. By extracting them here, we break the 6 * circular dependency between settings/types.ts and plugins/schemas.ts. 7 * 8 * Both files now import from this shared location instead of each other. 9 */ 10 11import { HOOK_EVENTS, type HookEvent } from 'src/entrypoints/agentSdkTypes.js' 12import { z } from 'zod/v4' 13import { lazySchema } from '../utils/lazySchema.js' 14import { SHELL_TYPES } from '../utils/shell/shellProvider.js' 15 16// Shared schema for the `if` condition field. 17// Uses permission rule syntax (e.g., "Bash(git *)", "Read(*.ts)") to filter hooks 18// before spawning. Evaluated against the hook input's tool_name and tool_input. 19const IfConditionSchema = lazySchema(() => 20 z 21 .string() 22 .optional() 23 .describe( 24 'Permission rule syntax to filter when this hook runs (e.g., "Bash(git *)"). ' + 25 'Only runs if the tool call matches the pattern. Avoids spawning hooks for non-matching commands.', 26 ), 27) 28 29// Internal factory for individual hook schemas (shared between exported 30// discriminated union members and the HookCommandSchema factory) 31function buildHookSchemas() { 32 const BashCommandHookSchema = z.object({ 33 type: z.literal('command').describe('Shell command hook type'), 34 command: z.string().describe('Shell command to execute'), 35 if: IfConditionSchema(), 36 shell: z 37 .enum(SHELL_TYPES) 38 .optional() 39 .describe( 40 "Shell interpreter. 'bash' uses your $SHELL (bash/zsh/sh); 'powershell' uses pwsh. Defaults to bash.", 41 ), 42 timeout: z 43 .number() 44 .positive() 45 .optional() 46 .describe('Timeout in seconds for this specific command'), 47 statusMessage: z 48 .string() 49 .optional() 50 .describe('Custom status message to display in spinner while hook runs'), 51 once: z 52 .boolean() 53 .optional() 54 .describe('If true, hook runs once and is removed after execution'), 55 async: z 56 .boolean() 57 .optional() 58 .describe('If true, hook runs in background without blocking'), 59 asyncRewake: z 60 .boolean() 61 .optional() 62 .describe( 63 'If true, hook runs in background and wakes the model on exit code 2 (blocking error). Implies async.', 64 ), 65 }) 66 67 const PromptHookSchema = z.object({ 68 type: z.literal('prompt').describe('LLM prompt hook type'), 69 prompt: z 70 .string() 71 .describe( 72 'Prompt to evaluate with LLM. Use $ARGUMENTS placeholder for hook input JSON.', 73 ), 74 if: IfConditionSchema(), 75 timeout: z 76 .number() 77 .positive() 78 .optional() 79 .describe('Timeout in seconds for this specific prompt evaluation'), 80 // @[MODEL LAUNCH]: Update the example model ID in the .describe() strings below (prompt + agent hooks). 81 model: z 82 .string() 83 .optional() 84 .describe( 85 'Model to use for this prompt hook (e.g., "claude-sonnet-4-6"). If not specified, uses the default small fast model.', 86 ), 87 statusMessage: z 88 .string() 89 .optional() 90 .describe('Custom status message to display in spinner while hook runs'), 91 once: z 92 .boolean() 93 .optional() 94 .describe('If true, hook runs once and is removed after execution'), 95 }) 96 97 const HttpHookSchema = z.object({ 98 type: z.literal('http').describe('HTTP hook type'), 99 url: z.string().url().describe('URL to POST the hook input JSON to'), 100 if: IfConditionSchema(), 101 timeout: z 102 .number() 103 .positive() 104 .optional() 105 .describe('Timeout in seconds for this specific request'), 106 headers: z 107 .record(z.string(), z.string()) 108 .optional() 109 .describe( 110 'Additional headers to include in the request. Values may reference environment variables using $VAR_NAME or ${VAR_NAME} syntax (e.g., "Authorization": "Bearer $MY_TOKEN"). Only variables listed in allowedEnvVars will be interpolated.', 111 ), 112 allowedEnvVars: z 113 .array(z.string()) 114 .optional() 115 .describe( 116 'Explicit list of environment variable names that may be interpolated in header values. Only variables listed here will be resolved; all other $VAR references are left as empty strings. Required for env var interpolation to work.', 117 ), 118 statusMessage: z 119 .string() 120 .optional() 121 .describe('Custom status message to display in spinner while hook runs'), 122 once: z 123 .boolean() 124 .optional() 125 .describe('If true, hook runs once and is removed after execution'), 126 }) 127 128 const AgentHookSchema = z.object({ 129 type: z.literal('agent').describe('Agentic verifier hook type'), 130 // DO NOT add .transform() here. This schema is used by parseSettingsFile, 131 // and updateSettingsForSource round-trips the parsed result through 132 // JSON.stringify — a transformed function value is silently dropped, 133 // deleting the user's prompt from settings.json (gh-24920, CC-79). The 134 // transform (from #10594) wrapped the string in `(_msgs) => prompt` 135 // for a programmatic-construction use case in ExitPlanModeV2Tool that 136 // has since been refactored into VerifyPlanExecutionTool, which no 137 // longer constructs AgentHook objects at all. 138 prompt: z 139 .string() 140 .describe( 141 'Prompt describing what to verify (e.g. "Verify that unit tests ran and passed."). Use $ARGUMENTS placeholder for hook input JSON.', 142 ), 143 if: IfConditionSchema(), 144 timeout: z 145 .number() 146 .positive() 147 .optional() 148 .describe('Timeout in seconds for agent execution (default 60)'), 149 model: z 150 .string() 151 .optional() 152 .describe( 153 'Model to use for this agent hook (e.g., "claude-sonnet-4-6"). If not specified, uses Haiku.', 154 ), 155 statusMessage: z 156 .string() 157 .optional() 158 .describe('Custom status message to display in spinner while hook runs'), 159 once: z 160 .boolean() 161 .optional() 162 .describe('If true, hook runs once and is removed after execution'), 163 }) 164 165 return { 166 BashCommandHookSchema, 167 PromptHookSchema, 168 HttpHookSchema, 169 AgentHookSchema, 170 } 171} 172 173/** 174 * Schema for hook command (excludes function hooks - they can't be persisted) 175 */ 176export const HookCommandSchema = lazySchema(() => { 177 const { 178 BashCommandHookSchema, 179 PromptHookSchema, 180 AgentHookSchema, 181 HttpHookSchema, 182 } = buildHookSchemas() 183 return z.discriminatedUnion('type', [ 184 BashCommandHookSchema, 185 PromptHookSchema, 186 AgentHookSchema, 187 HttpHookSchema, 188 ]) 189}) 190 191/** 192 * Schema for matcher configuration with multiple hooks 193 */ 194export const HookMatcherSchema = lazySchema(() => 195 z.object({ 196 matcher: z 197 .string() 198 .optional() 199 .describe('String pattern to match (e.g. tool names like "Write")'), // String (e.g. Write) to match values related to the hook event, e.g. tool names 200 hooks: z 201 .array(HookCommandSchema()) 202 .describe('List of hooks to execute when the matcher matches'), 203 }), 204) 205 206/** 207 * Schema for hooks configuration 208 * The key is the hook event. The value is an array of matcher configurations. 209 * Uses partialRecord since not all hook events need to be defined. 210 */ 211export const HooksSchema = lazySchema(() => 212 z.partialRecord(z.enum(HOOK_EVENTS), z.array(HookMatcherSchema())), 213) 214 215// Inferred types from schemas 216export type HookCommand = z.infer<ReturnType<typeof HookCommandSchema>> 217export type BashCommandHook = Extract<HookCommand, { type: 'command' }> 218export type PromptHook = Extract<HookCommand, { type: 'prompt' }> 219export type AgentHook = Extract<HookCommand, { type: 'agent' }> 220export type HttpHook = Extract<HookCommand, { type: 'http' }> 221export type HookMatcher = z.infer<ReturnType<typeof HookMatcherSchema>> 222export type HooksSettings = Partial<Record<HookEvent, HookMatcher[]>>