| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { HOOK_EVENTS, type HookEvent } from 'src/entrypoints/agentSdkTypes.js' |
| import { z } from 'zod/v4' |
| import { lazySchema } from '../utils/lazySchema.js' |
| import { SHELL_TYPES } from '../utils/shell/shellProvider.js' |
|
|
| |
| |
| |
| const IfConditionSchema = lazySchema(() => |
| z |
| .string() |
| .optional() |
| .describe( |
| 'Permission rule syntax to filter when this hook runs (e.g., "Bash(git *)"). ' + |
| 'Only runs if the tool call matches the pattern. Avoids spawning hooks for non-matching commands.', |
| ), |
| ) |
|
|
| |
| |
| function buildHookSchemas() { |
| const BashCommandHookSchema = z.object({ |
| type: z.literal('command').describe('Shell command hook type'), |
| command: z.string().describe('Shell command to execute'), |
| if: IfConditionSchema(), |
| shell: z |
| .enum(SHELL_TYPES) |
| .optional() |
| .describe( |
| "Shell interpreter. 'bash' uses your $SHELL (bash/zsh/sh); 'powershell' uses pwsh. Defaults to bash.", |
| ), |
| timeout: z |
| .number() |
| .positive() |
| .optional() |
| .describe('Timeout in seconds for this specific command'), |
| statusMessage: z |
| .string() |
| .optional() |
| .describe('Custom status message to display in spinner while hook runs'), |
| once: z |
| .boolean() |
| .optional() |
| .describe('If true, hook runs once and is removed after execution'), |
| async: z |
| .boolean() |
| .optional() |
| .describe('If true, hook runs in background without blocking'), |
| asyncRewake: z |
| .boolean() |
| .optional() |
| .describe( |
| 'If true, hook runs in background and wakes the model on exit code 2 (blocking error). Implies async.', |
| ), |
| }) |
|
|
| const PromptHookSchema = z.object({ |
| type: z.literal('prompt').describe('LLM prompt hook type'), |
| prompt: z |
| .string() |
| .describe( |
| 'Prompt to evaluate with LLM. Use $ARGUMENTS placeholder for hook input JSON.', |
| ), |
| if: IfConditionSchema(), |
| timeout: z |
| .number() |
| .positive() |
| .optional() |
| .describe('Timeout in seconds for this specific prompt evaluation'), |
| |
| model: z |
| .string() |
| .optional() |
| .describe( |
| 'Model to use for this prompt hook (e.g., "claude-sonnet-4-6"). If not specified, uses the default small fast model.', |
| ), |
| statusMessage: z |
| .string() |
| .optional() |
| .describe('Custom status message to display in spinner while hook runs'), |
| once: z |
| .boolean() |
| .optional() |
| .describe('If true, hook runs once and is removed after execution'), |
| }) |
|
|
| const HttpHookSchema = z.object({ |
| type: z.literal('http').describe('HTTP hook type'), |
| url: z.string().url().describe('URL to POST the hook input JSON to'), |
| if: IfConditionSchema(), |
| timeout: z |
| .number() |
| .positive() |
| .optional() |
| .describe('Timeout in seconds for this specific request'), |
| headers: z |
| .record(z.string(), z.string()) |
| .optional() |
| .describe( |
| '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.', |
| ), |
| allowedEnvVars: z |
| .array(z.string()) |
| .optional() |
| .describe( |
| '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.', |
| ), |
| statusMessage: z |
| .string() |
| .optional() |
| .describe('Custom status message to display in spinner while hook runs'), |
| once: z |
| .boolean() |
| .optional() |
| .describe('If true, hook runs once and is removed after execution'), |
| }) |
|
|
| const AgentHookSchema = z.object({ |
| type: z.literal('agent').describe('Agentic verifier hook type'), |
| |
| |
| |
| |
| |
| |
| |
| |
| prompt: z |
| .string() |
| .describe( |
| 'Prompt describing what to verify (e.g. "Verify that unit tests ran and passed."). Use $ARGUMENTS placeholder for hook input JSON.', |
| ), |
| if: IfConditionSchema(), |
| timeout: z |
| .number() |
| .positive() |
| .optional() |
| .describe('Timeout in seconds for agent execution (default 60)'), |
| model: z |
| .string() |
| .optional() |
| .describe( |
| 'Model to use for this agent hook (e.g., "claude-sonnet-4-6"). If not specified, uses Haiku.', |
| ), |
| statusMessage: z |
| .string() |
| .optional() |
| .describe('Custom status message to display in spinner while hook runs'), |
| once: z |
| .boolean() |
| .optional() |
| .describe('If true, hook runs once and is removed after execution'), |
| }) |
|
|
| return { |
| BashCommandHookSchema, |
| PromptHookSchema, |
| HttpHookSchema, |
| AgentHookSchema, |
| } |
| } |
|
|
| |
| |
| |
| export const HookCommandSchema = lazySchema(() => { |
| const { |
| BashCommandHookSchema, |
| PromptHookSchema, |
| AgentHookSchema, |
| HttpHookSchema, |
| } = buildHookSchemas() |
| return z.discriminatedUnion('type', [ |
| BashCommandHookSchema, |
| PromptHookSchema, |
| AgentHookSchema, |
| HttpHookSchema, |
| ]) |
| }) |
|
|
| |
| |
| |
| export const HookMatcherSchema = lazySchema(() => |
| z.object({ |
| matcher: z |
| .string() |
| .optional() |
| .describe('String pattern to match (e.g. tool names like "Write")'), |
| hooks: z |
| .array(HookCommandSchema()) |
| .describe('List of hooks to execute when the matcher matches'), |
| }), |
| ) |
|
|
| |
| |
| |
| |
| |
| export const HooksSchema = lazySchema(() => |
| z.partialRecord(z.enum(HOOK_EVENTS), z.array(HookMatcherSchema())), |
| ) |
|
|
| |
| export type HookCommand = z.infer<ReturnType<typeof HookCommandSchema>> |
| export type BashCommandHook = Extract<HookCommand, { type: 'command' }> |
| export type PromptHook = Extract<HookCommand, { type: 'prompt' }> |
| export type AgentHook = Extract<HookCommand, { type: 'agent' }> |
| export type HttpHook = Extract<HookCommand, { type: 'http' }> |
| export type HookMatcher = z.infer<ReturnType<typeof HookMatcherSchema>> |
| export type HooksSettings = Partial<Record<HookEvent, HookMatcher[]>> |
|
|