source dump of claude code
at main 400 lines 18 kB view raw
1import memoize from 'lodash-es/memoize.js' 2import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js' 3import { getRegisteredHooks } from '../../bootstrap/state.js' 4import type { AppState } from '../../state/AppState.js' 5import { 6 getAllHooks, 7 type IndividualHookConfig, 8 sortMatchersByPriority, 9} from './hooksSettings.js' 10 11export type MatcherMetadata = { 12 fieldToMatch: string 13 values: string[] 14} 15 16export type HookEventMetadata = { 17 summary: string 18 description: string 19 matcherMetadata?: MatcherMetadata 20} 21 22// Hook event metadata configuration. 23// Resolver uses sorted-joined string key so that callers passing a fresh 24// toolNames array each render (e.g. HooksConfigMenu) hit the cache instead 25// of leaking a new entry per call. 26export const getHookEventMetadata = memoize( 27 function (toolNames: string[]): Record<HookEvent, HookEventMetadata> { 28 return { 29 PreToolUse: { 30 summary: 'Before tool execution', 31 description: 32 'Input to command is JSON of tool call arguments.\nExit code 0 - stdout/stderr not shown\nExit code 2 - show stderr to model and block tool call\nOther exit codes - show stderr to user only but continue with tool call', 33 matcherMetadata: { 34 fieldToMatch: 'tool_name', 35 values: toolNames, 36 }, 37 }, 38 PostToolUse: { 39 summary: 'After tool execution', 40 description: 41 'Input to command is JSON with fields "inputs" (tool call arguments) and "response" (tool call response).\nExit code 0 - stdout shown in transcript mode (ctrl+o)\nExit code 2 - show stderr to model immediately\nOther exit codes - show stderr to user only', 42 matcherMetadata: { 43 fieldToMatch: 'tool_name', 44 values: toolNames, 45 }, 46 }, 47 PostToolUseFailure: { 48 summary: 'After tool execution fails', 49 description: 50 'Input to command is JSON with tool_name, tool_input, tool_use_id, error, error_type, is_interrupt, and is_timeout.\nExit code 0 - stdout shown in transcript mode (ctrl+o)\nExit code 2 - show stderr to model immediately\nOther exit codes - show stderr to user only', 51 matcherMetadata: { 52 fieldToMatch: 'tool_name', 53 values: toolNames, 54 }, 55 }, 56 PermissionDenied: { 57 summary: 'After auto mode classifier denies a tool call', 58 description: 59 'Input to command is JSON with tool_name, tool_input, tool_use_id, and reason.\nReturn {"hookSpecificOutput":{"hookEventName":"PermissionDenied","retry":true}} to tell the model it may retry.\nExit code 0 - stdout shown in transcript mode (ctrl+o)\nOther exit codes - show stderr to user only', 60 matcherMetadata: { 61 fieldToMatch: 'tool_name', 62 values: toolNames, 63 }, 64 }, 65 Notification: { 66 summary: 'When notifications are sent', 67 description: 68 'Input to command is JSON with notification message and type.\nExit code 0 - stdout/stderr not shown\nOther exit codes - show stderr to user only', 69 matcherMetadata: { 70 fieldToMatch: 'notification_type', 71 values: [ 72 'permission_prompt', 73 'idle_prompt', 74 'auth_success', 75 'elicitation_dialog', 76 'elicitation_complete', 77 'elicitation_response', 78 ], 79 }, 80 }, 81 UserPromptSubmit: { 82 summary: 'When the user submits a prompt', 83 description: 84 'Input to command is JSON with original user prompt text.\nExit code 0 - stdout shown to Claude\nExit code 2 - block processing, erase original prompt, and show stderr to user only\nOther exit codes - show stderr to user only', 85 }, 86 SessionStart: { 87 summary: 'When a new session is started', 88 description: 89 'Input to command is JSON with session start source.\nExit code 0 - stdout shown to Claude\nBlocking errors are ignored\nOther exit codes - show stderr to user only', 90 matcherMetadata: { 91 fieldToMatch: 'source', 92 values: ['startup', 'resume', 'clear', 'compact'], 93 }, 94 }, 95 Stop: { 96 summary: 'Right before Claude concludes its response', 97 description: 98 'Exit code 0 - stdout/stderr not shown\nExit code 2 - show stderr to model and continue conversation\nOther exit codes - show stderr to user only', 99 }, 100 StopFailure: { 101 summary: 'When the turn ends due to an API error', 102 description: 103 'Fires instead of Stop when an API error (rate limit, auth failure, etc.) ended the turn. Fire-and-forget — hook output and exit codes are ignored.', 104 matcherMetadata: { 105 fieldToMatch: 'error', 106 values: [ 107 'rate_limit', 108 'authentication_failed', 109 'billing_error', 110 'invalid_request', 111 'server_error', 112 'max_output_tokens', 113 'unknown', 114 ], 115 }, 116 }, 117 SubagentStart: { 118 summary: 'When a subagent (Agent tool call) is started', 119 description: 120 'Input to command is JSON with agent_id and agent_type.\nExit code 0 - stdout shown to subagent\nBlocking errors are ignored\nOther exit codes - show stderr to user only', 121 matcherMetadata: { 122 fieldToMatch: 'agent_type', 123 values: [], // Will be populated with available agent types 124 }, 125 }, 126 SubagentStop: { 127 summary: 128 'Right before a subagent (Agent tool call) concludes its response', 129 description: 130 'Input to command is JSON with agent_id, agent_type, and agent_transcript_path.\nExit code 0 - stdout/stderr not shown\nExit code 2 - show stderr to subagent and continue having it run\nOther exit codes - show stderr to user only', 131 matcherMetadata: { 132 fieldToMatch: 'agent_type', 133 values: [], // Will be populated with available agent types 134 }, 135 }, 136 PreCompact: { 137 summary: 'Before conversation compaction', 138 description: 139 'Input to command is JSON with compaction details.\nExit code 0 - stdout appended as custom compact instructions\nExit code 2 - block compaction\nOther exit codes - show stderr to user only but continue with compaction', 140 matcherMetadata: { 141 fieldToMatch: 'trigger', 142 values: ['manual', 'auto'], 143 }, 144 }, 145 PostCompact: { 146 summary: 'After conversation compaction', 147 description: 148 'Input to command is JSON with compaction details and the summary.\nExit code 0 - stdout shown to user\nOther exit codes - show stderr to user only', 149 matcherMetadata: { 150 fieldToMatch: 'trigger', 151 values: ['manual', 'auto'], 152 }, 153 }, 154 SessionEnd: { 155 summary: 'When a session is ending', 156 description: 157 'Input to command is JSON with session end reason.\nExit code 0 - command completes successfully\nOther exit codes - show stderr to user only', 158 matcherMetadata: { 159 fieldToMatch: 'reason', 160 values: ['clear', 'logout', 'prompt_input_exit', 'other'], 161 }, 162 }, 163 PermissionRequest: { 164 summary: 'When a permission dialog is displayed', 165 description: 166 'Input to command is JSON with tool_name, tool_input, and tool_use_id.\nOutput JSON with hookSpecificOutput containing decision to allow or deny.\nExit code 0 - use hook decision if provided\nOther exit codes - show stderr to user only', 167 matcherMetadata: { 168 fieldToMatch: 'tool_name', 169 values: toolNames, 170 }, 171 }, 172 Setup: { 173 summary: 'Repo setup hooks for init and maintenance', 174 description: 175 'Input to command is JSON with trigger (init or maintenance).\nExit code 0 - stdout shown to Claude\nBlocking errors are ignored\nOther exit codes - show stderr to user only', 176 matcherMetadata: { 177 fieldToMatch: 'trigger', 178 values: ['init', 'maintenance'], 179 }, 180 }, 181 TeammateIdle: { 182 summary: 'When a teammate is about to go idle', 183 description: 184 'Input to command is JSON with teammate_name and team_name.\nExit code 0 - stdout/stderr not shown\nExit code 2 - show stderr to teammate and prevent idle (teammate continues working)\nOther exit codes - show stderr to user only', 185 }, 186 TaskCreated: { 187 summary: 'When a task is being created', 188 description: 189 'Input to command is JSON with task_id, task_subject, task_description, teammate_name, and team_name.\nExit code 0 - stdout/stderr not shown\nExit code 2 - show stderr to model and prevent task creation\nOther exit codes - show stderr to user only', 190 }, 191 TaskCompleted: { 192 summary: 'When a task is being marked as completed', 193 description: 194 'Input to command is JSON with task_id, task_subject, task_description, teammate_name, and team_name.\nExit code 0 - stdout/stderr not shown\nExit code 2 - show stderr to model and prevent task completion\nOther exit codes - show stderr to user only', 195 }, 196 Elicitation: { 197 summary: 'When an MCP server requests user input (elicitation)', 198 description: 199 'Input to command is JSON with mcp_server_name, message, and requested_schema.\nOutput JSON with hookSpecificOutput containing action (accept/decline/cancel) and optional content.\nExit code 0 - use hook response if provided\nExit code 2 - deny the elicitation\nOther exit codes - show stderr to user only', 200 matcherMetadata: { 201 fieldToMatch: 'mcp_server_name', 202 values: [], 203 }, 204 }, 205 ElicitationResult: { 206 summary: 'After a user responds to an MCP elicitation', 207 description: 208 'Input to command is JSON with mcp_server_name, action, content, mode, and elicitation_id.\nOutput JSON with hookSpecificOutput containing optional action and content to override the response.\nExit code 0 - use hook response if provided\nExit code 2 - block the response (action becomes decline)\nOther exit codes - show stderr to user only', 209 matcherMetadata: { 210 fieldToMatch: 'mcp_server_name', 211 values: [], 212 }, 213 }, 214 ConfigChange: { 215 summary: 'When configuration files change during a session', 216 description: 217 'Input to command is JSON with source (user_settings, project_settings, local_settings, policy_settings, skills) and file_path.\nExit code 0 - allow the change\nExit code 2 - block the change from being applied to the session\nOther exit codes - show stderr to user only', 218 matcherMetadata: { 219 fieldToMatch: 'source', 220 values: [ 221 'user_settings', 222 'project_settings', 223 'local_settings', 224 'policy_settings', 225 'skills', 226 ], 227 }, 228 }, 229 InstructionsLoaded: { 230 summary: 'When an instruction file (CLAUDE.md or rule) is loaded', 231 description: 232 'Input to command is JSON with file_path, memory_type (User, Project, Local, Managed), load_reason (session_start, nested_traversal, path_glob_match, include, compact), globs (optional — the paths: frontmatter patterns that matched), trigger_file_path (optional — the file Claude touched that caused the load), and parent_file_path (optional — the file that @-included this one).\nExit code 0 - command completes successfully\nOther exit codes - show stderr to user only\nThis hook is observability-only and does not support blocking.', 233 matcherMetadata: { 234 fieldToMatch: 'load_reason', 235 values: [ 236 'session_start', 237 'nested_traversal', 238 'path_glob_match', 239 'include', 240 'compact', 241 ], 242 }, 243 }, 244 WorktreeCreate: { 245 summary: 'Create an isolated worktree for VCS-agnostic isolation', 246 description: 247 'Input to command is JSON with name (suggested worktree slug).\nStdout should contain the absolute path to the created worktree directory.\nExit code 0 - worktree created successfully\nOther exit codes - worktree creation failed', 248 }, 249 WorktreeRemove: { 250 summary: 'Remove a previously created worktree', 251 description: 252 'Input to command is JSON with worktree_path (absolute path to worktree).\nExit code 0 - worktree removed successfully\nOther exit codes - show stderr to user only', 253 }, 254 CwdChanged: { 255 summary: 'After the working directory changes', 256 description: 257 'Input to command is JSON with old_cwd and new_cwd.\nCLAUDE_ENV_FILE is set — write bash exports there to apply env to subsequent BashTool commands.\nHook output can include hookSpecificOutput.watchPaths (array of absolute paths) to register with the FileChanged watcher.\nExit code 0 - command completes successfully\nOther exit codes - show stderr to user only', 258 }, 259 FileChanged: { 260 summary: 'When a watched file changes', 261 description: 262 'Input to command is JSON with file_path and event (change, add, unlink).\nCLAUDE_ENV_FILE is set — write bash exports there to apply env to subsequent BashTool commands.\nThe matcher field specifies filenames to watch in the current directory (e.g. ".envrc|.env").\nHook output can include hookSpecificOutput.watchPaths (array of absolute paths) to dynamically update the watch list.\nExit code 0 - command completes successfully\nOther exit codes - show stderr to user only', 263 }, 264 } 265 }, 266 toolNames => toolNames.slice().sort().join(','), 267) 268 269// Group hooks by event and matcher 270export function groupHooksByEventAndMatcher( 271 appState: AppState, 272 toolNames: string[], 273): Record<HookEvent, Record<string, IndividualHookConfig[]>> { 274 const grouped: Record<HookEvent, Record<string, IndividualHookConfig[]>> = { 275 PreToolUse: {}, 276 PostToolUse: {}, 277 PostToolUseFailure: {}, 278 PermissionDenied: {}, 279 Notification: {}, 280 UserPromptSubmit: {}, 281 SessionStart: {}, 282 SessionEnd: {}, 283 Stop: {}, 284 StopFailure: {}, 285 SubagentStart: {}, 286 SubagentStop: {}, 287 PreCompact: {}, 288 PostCompact: {}, 289 PermissionRequest: {}, 290 Setup: {}, 291 TeammateIdle: {}, 292 TaskCreated: {}, 293 TaskCompleted: {}, 294 Elicitation: {}, 295 ElicitationResult: {}, 296 ConfigChange: {}, 297 WorktreeCreate: {}, 298 WorktreeRemove: {}, 299 InstructionsLoaded: {}, 300 CwdChanged: {}, 301 FileChanged: {}, 302 } 303 304 const metadata = getHookEventMetadata(toolNames) 305 306 // Include hooks from settings files 307 getAllHooks(appState).forEach(hook => { 308 const eventGroup = grouped[hook.event] 309 if (eventGroup) { 310 // For events without matchers, use empty string as key 311 const matcherKey = 312 metadata[hook.event].matcherMetadata !== undefined 313 ? hook.matcher || '' 314 : '' 315 if (!eventGroup[matcherKey]) { 316 eventGroup[matcherKey] = [] 317 } 318 eventGroup[matcherKey].push(hook) 319 } 320 }) 321 322 // Include registered hooks (e.g., plugin hooks) 323 const registeredHooks = getRegisteredHooks() 324 if (registeredHooks) { 325 for (const [event, matchers] of Object.entries(registeredHooks)) { 326 const hookEvent = event as HookEvent 327 const eventGroup = grouped[hookEvent] 328 if (!eventGroup) continue 329 330 for (const matcher of matchers) { 331 const matcherKey = matcher.matcher || '' 332 333 // Only PluginHookMatcher has pluginRoot; HookCallbackMatcher (internal 334 // callbacks like attributionHooks, sessionFileAccessHooks) does not. 335 if ('pluginRoot' in matcher) { 336 eventGroup[matcherKey] ??= [] 337 for (const hook of matcher.hooks) { 338 eventGroup[matcherKey].push({ 339 event: hookEvent, 340 config: hook, 341 matcher: matcher.matcher, 342 source: 'pluginHook', 343 pluginName: matcher.pluginId, 344 }) 345 } 346 } else if (process.env.USER_TYPE === 'ant') { 347 eventGroup[matcherKey] ??= [] 348 for (const _hook of matcher.hooks) { 349 eventGroup[matcherKey].push({ 350 event: hookEvent, 351 config: { 352 type: 'command', 353 command: '[ANT-ONLY] Built-in Hook', 354 }, 355 matcher: matcher.matcher, 356 source: 'builtinHook', 357 }) 358 } 359 } 360 } 361 } 362 } 363 364 return grouped 365} 366 367// Get sorted matchers for a specific event 368export function getSortedMatchersForEvent( 369 hooksByEventAndMatcher: Record< 370 HookEvent, 371 Record<string, IndividualHookConfig[]> 372 >, 373 event: HookEvent, 374): string[] { 375 const matchers = Object.keys(hooksByEventAndMatcher[event] || {}) 376 return sortMatchersByPriority(matchers, hooksByEventAndMatcher, event) 377} 378 379// Get hooks for a specific event and matcher 380export function getHooksForMatcher( 381 hooksByEventAndMatcher: Record< 382 HookEvent, 383 Record<string, IndividualHookConfig[]> 384 >, 385 event: HookEvent, 386 matcher: string | null, 387): IndividualHookConfig[] { 388 // For events without matchers, hooks are stored with empty string as key 389 // because the record keys must be strings. 390 const matcherKey = matcher ?? '' 391 return hooksByEventAndMatcher[event]?.[matcherKey] ?? [] 392} 393 394// Get metadata for a specific event's matcher 395export function getMatcherMetadata( 396 event: HookEvent, 397 toolNames: string[], 398): MatcherMetadata | undefined { 399 return getHookEventMetadata(toolNames)[event].matcherMetadata 400}