1#![allow(dead_code)]
3
4use std::collections::HashMap;
5
6#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
8pub enum HookEvent {
9 PreToolUse,
10 PostToolUse,
11 PostToolUseFailure,
12 PermissionDenied,
13 Notification,
14 UserPromptSubmit,
15 SessionStart,
16 SessionEnd,
17 Stop,
18 StopFailure,
19 SubagentStart,
20 SubagentStop,
21 PreCompact,
22 PostCompact,
23 PermissionRequest,
24 Setup,
25 TeammateIdle,
26 TaskCreated,
27 TaskCompleted,
28 Elicitation,
29 ElicitationResult,
30 ConfigChange,
31 WorktreeCreate,
32 WorktreeRemove,
33 InstructionsLoaded,
34 CwdChanged,
35 FileChanged,
36}
37
38impl HookEvent {
39 pub fn as_str(&self) -> &'static str {
40 match self {
41 HookEvent::PreToolUse => "PreToolUse",
42 HookEvent::PostToolUse => "PostToolUse",
43 HookEvent::PostToolUseFailure => "PostToolUseFailure",
44 HookEvent::PermissionDenied => "PermissionDenied",
45 HookEvent::Notification => "Notification",
46 HookEvent::UserPromptSubmit => "UserPromptSubmit",
47 HookEvent::SessionStart => "SessionStart",
48 HookEvent::SessionEnd => "SessionEnd",
49 HookEvent::Stop => "Stop",
50 HookEvent::StopFailure => "StopFailure",
51 HookEvent::SubagentStart => "SubagentStart",
52 HookEvent::SubagentStop => "SubagentStop",
53 HookEvent::PreCompact => "PreCompact",
54 HookEvent::PostCompact => "PostCompact",
55 HookEvent::PermissionRequest => "PermissionRequest",
56 HookEvent::Setup => "Setup",
57 HookEvent::TeammateIdle => "TeammateIdle",
58 HookEvent::TaskCreated => "TaskCreated",
59 HookEvent::TaskCompleted => "TaskCompleted",
60 HookEvent::Elicitation => "Elicitation",
61 HookEvent::ElicitationResult => "ElicitationResult",
62 HookEvent::ConfigChange => "ConfigChange",
63 HookEvent::WorktreeCreate => "WorktreeCreate",
64 HookEvent::WorktreeRemove => "WorktreeRemove",
65 HookEvent::InstructionsLoaded => "InstructionsLoaded",
66 HookEvent::CwdChanged => "CwdChanged",
67 HookEvent::FileChanged => "FileChanged",
68 }
69 }
70}
71
72pub const HOOK_EVENTS: &[HookEvent] = &[
74 HookEvent::PreToolUse,
75 HookEvent::PostToolUse,
76 HookEvent::PostToolUseFailure,
77 HookEvent::PermissionDenied,
78 HookEvent::Notification,
79 HookEvent::UserPromptSubmit,
80 HookEvent::SessionStart,
81 HookEvent::SessionEnd,
82 HookEvent::Stop,
83 HookEvent::StopFailure,
84 HookEvent::SubagentStart,
85 HookEvent::SubagentStop,
86 HookEvent::PreCompact,
87 HookEvent::PostCompact,
88 HookEvent::PermissionRequest,
89 HookEvent::Setup,
90 HookEvent::TeammateIdle,
91 HookEvent::TaskCreated,
92 HookEvent::TaskCompleted,
93 HookEvent::Elicitation,
94 HookEvent::ElicitationResult,
95 HookEvent::ConfigChange,
96 HookEvent::WorktreeCreate,
97 HookEvent::WorktreeRemove,
98 HookEvent::InstructionsLoaded,
99 HookEvent::CwdChanged,
100 HookEvent::FileChanged,
101];
102
103#[derive(Debug, Clone)]
105pub struct MatcherMetadata {
106 pub field_to_match: String,
107 pub values: Vec<String>,
108}
109
110#[derive(Debug, Clone)]
112pub struct HookEventMetadata {
113 pub summary: String,
114 pub description: String,
115 pub matcher_metadata: Option<MatcherMetadata>,
116}
117
118#[derive(Debug, Clone)]
120pub struct IndividualHookConfig {
121 pub event: HookEvent,
122 pub config: HookCommand,
123 pub matcher: Option<String>,
124 pub source: HookSource,
125 pub plugin_name: Option<String>,
126}
127
128#[derive(Debug, Clone)]
130pub enum HookCommand {
131 Command {
132 command: String,
133 shell: Option<String>,
134 if_condition: Option<String>,
135 timeout: Option<u64>,
136 },
137 Prompt {
138 prompt: String,
139 if_condition: Option<String>,
140 timeout: Option<u64>,
141 },
142 Agent {
143 prompt: String,
144 model: Option<String>,
145 if_condition: Option<String>,
146 timeout: Option<u64>,
147 },
148 Http {
149 url: String,
150 if_condition: Option<String>,
151 timeout: Option<u64>,
152 },
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, Hash)]
157pub enum HookSource {
158 UserSettings,
159 ProjectSettings,
160 LocalSettings,
161 PluginHook,
162 SessionHook,
163 BuiltinHook,
164}
165
166impl std::fmt::Display for HookSource {
167 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168 match self {
169 HookSource::UserSettings => write!(f, "User Settings"),
170 HookSource::ProjectSettings => write!(f, "Project Settings"),
171 HookSource::LocalSettings => write!(f, "Local Settings"),
172 HookSource::PluginHook => write!(f, "Plugin Hooks"),
173 HookSource::SessionHook => write!(f, "Session Hooks"),
174 HookSource::BuiltinHook => write!(f, "Built-in Hooks"),
175 }
176 }
177}
178
179pub fn get_hook_display_text(hook: &HookCommand) -> String {
181 match hook {
182 HookCommand::Command { command, .. } => command.clone(),
183 HookCommand::Prompt { prompt, .. } => prompt.clone(),
184 HookCommand::Agent { prompt, .. } => prompt.clone(),
185 HookCommand::Http { url, .. } => url.clone(),
186 }
187}
188
189pub fn get_hook_event_metadata(tool_names: &[String]) -> HashMap<HookEvent, HookEventMetadata> {
191 let mut metadata = HashMap::new();
192
193 metadata.insert(
194 HookEvent::PreToolUse,
195 HookEventMetadata {
196 summary: "Before tool execution".to_string(),
197 description: "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".to_string(),
198 matcher_metadata: Some(MatcherMetadata {
199 field_to_match: "tool_name".to_string(),
200 values: tool_names.to_vec(),
201 }),
202 },
203 );
204
205 metadata.insert(
206 HookEvent::PostToolUse,
207 HookEventMetadata {
208 summary: "After tool execution".to_string(),
209 description: "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".to_string(),
210 matcher_metadata: Some(MatcherMetadata {
211 field_to_match: "tool_name".to_string(),
212 values: tool_names.to_vec(),
213 }),
214 },
215 );
216
217 metadata.insert(
218 HookEvent::PostToolUseFailure,
219 HookEventMetadata {
220 summary: "After tool execution fails".to_string(),
221 description: "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".to_string(),
222 matcher_metadata: Some(MatcherMetadata {
223 field_to_match: "tool_name".to_string(),
224 values: tool_names.to_vec(),
225 }),
226 },
227 );
228
229 metadata.insert(
230 HookEvent::PermissionDenied,
231 HookEventMetadata {
232 summary: "After auto mode classifier denies a tool call".to_string(),
233 description: "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".to_string(),
234 matcher_metadata: Some(MatcherMetadata {
235 field_to_match: "tool_name".to_string(),
236 values: tool_names.to_vec(),
237 }),
238 },
239 );
240
241 metadata.insert(
242 HookEvent::Notification,
243 HookEventMetadata {
244 summary: "When notifications are sent".to_string(),
245 description: "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".to_string(),
246 matcher_metadata: Some(MatcherMetadata {
247 field_to_match: "notification_type".to_string(),
248 values: vec![
249 "permission_prompt".to_string(),
250 "idle_prompt".to_string(),
251 "auth_success".to_string(),
252 "elicitation_dialog".to_string(),
253 "elicitation_complete".to_string(),
254 "elicitation_response".to_string(),
255 ],
256 }),
257 },
258 );
259
260 metadata.insert(
261 HookEvent::UserPromptSubmit,
262 HookEventMetadata {
263 summary: "When the user submits a prompt".to_string(),
264 description: "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".to_string(),
265 matcher_metadata: None,
266 },
267 );
268
269 metadata.insert(
270 HookEvent::SessionStart,
271 HookEventMetadata {
272 summary: "When a new session is started".to_string(),
273 description: "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".to_string(),
274 matcher_metadata: Some(MatcherMetadata {
275 field_to_match: "source".to_string(),
276 values: vec![
277 "startup".to_string(),
278 "resume".to_string(),
279 "clear".to_string(),
280 "compact".to_string(),
281 ],
282 }),
283 },
284 );
285
286 metadata.insert(
287 HookEvent::Stop,
288 HookEventMetadata {
289 summary: "Right before Claude concludes its response".to_string(),
290 description: "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".to_string(),
291 matcher_metadata: None,
292 },
293 );
294
295 metadata.insert(
296 HookEvent::StopFailure,
297 HookEventMetadata {
298 summary: "When the turn ends due to an API error".to_string(),
299 description: "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.".to_string(),
300 matcher_metadata: Some(MatcherMetadata {
301 field_to_match: "error".to_string(),
302 values: vec![
303 "rate_limit".to_string(),
304 "authentication_failed".to_string(),
305 "billing_error".to_string(),
306 "invalid_request".to_string(),
307 "server_error".to_string(),
308 "max_output_tokens".to_string(),
309 "unknown".to_string(),
310 ],
311 }),
312 },
313 );
314
315 metadata.insert(
316 HookEvent::SubagentStart,
317 HookEventMetadata {
318 summary: "When a subagent (Agent tool call) is started".to_string(),
319 description: "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".to_string(),
320 matcher_metadata: Some(MatcherMetadata {
321 field_to_match: "agent_type".to_string(),
322 values: Vec::new(),
323 }),
324 },
325 );
326
327 metadata.insert(
328 HookEvent::SubagentStop,
329 HookEventMetadata {
330 summary: "Right before a subagent (Agent tool call) concludes its response".to_string(),
331 description: "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".to_string(),
332 matcher_metadata: Some(MatcherMetadata {
333 field_to_match: "agent_type".to_string(),
334 values: Vec::new(),
335 }),
336 },
337 );
338
339 metadata.insert(
340 HookEvent::PreCompact,
341 HookEventMetadata {
342 summary: "Before conversation compaction".to_string(),
343 description: "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".to_string(),
344 matcher_metadata: Some(MatcherMetadata {
345 field_to_match: "trigger".to_string(),
346 values: vec!["manual".to_string(), "auto".to_string()],
347 }),
348 },
349 );
350
351 metadata.insert(
352 HookEvent::PostCompact,
353 HookEventMetadata {
354 summary: "After conversation compaction".to_string(),
355 description: "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".to_string(),
356 matcher_metadata: Some(MatcherMetadata {
357 field_to_match: "trigger".to_string(),
358 values: vec!["manual".to_string(), "auto".to_string()],
359 }),
360 },
361 );
362
363 metadata.insert(
364 HookEvent::SessionEnd,
365 HookEventMetadata {
366 summary: "When a session is ending".to_string(),
367 description: "Input to command is JSON with session end reason.\nExit code 0 - command completes successfully\nOther exit codes - show stderr to user only".to_string(),
368 matcher_metadata: Some(MatcherMetadata {
369 field_to_match: "reason".to_string(),
370 values: vec![
371 "clear".to_string(),
372 "logout".to_string(),
373 "prompt_input_exit".to_string(),
374 "other".to_string(),
375 ],
376 }),
377 },
378 );
379
380 metadata.insert(
381 HookEvent::PermissionRequest,
382 HookEventMetadata {
383 summary: "When a permission dialog is displayed".to_string(),
384 description: "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".to_string(),
385 matcher_metadata: Some(MatcherMetadata {
386 field_to_match: "tool_name".to_string(),
387 values: tool_names.to_vec(),
388 }),
389 },
390 );
391
392 metadata.insert(
393 HookEvent::Setup,
394 HookEventMetadata {
395 summary: "Repo setup hooks for init and maintenance".to_string(),
396 description: "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".to_string(),
397 matcher_metadata: Some(MatcherMetadata {
398 field_to_match: "trigger".to_string(),
399 values: vec!["init".to_string(), "maintenance".to_string()],
400 }),
401 },
402 );
403
404 metadata.insert(
405 HookEvent::TeammateIdle,
406 HookEventMetadata {
407 summary: "When a teammate is about to go idle".to_string(),
408 description: "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".to_string(),
409 matcher_metadata: None,
410 },
411 );
412
413 metadata.insert(
414 HookEvent::TaskCreated,
415 HookEventMetadata {
416 summary: "When a task is being created".to_string(),
417 description: "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".to_string(),
418 matcher_metadata: None,
419 },
420 );
421
422 metadata.insert(
423 HookEvent::TaskCompleted,
424 HookEventMetadata {
425 summary: "When a task is being marked as completed".to_string(),
426 description: "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".to_string(),
427 matcher_metadata: None,
428 },
429 );
430
431 metadata.insert(
432 HookEvent::Elicitation,
433 HookEventMetadata {
434 summary: "When an MCP server requests user input (elicitation)".to_string(),
435 description: "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".to_string(),
436 matcher_metadata: Some(MatcherMetadata {
437 field_to_match: "mcp_server_name".to_string(),
438 values: Vec::new(),
439 }),
440 },
441 );
442
443 metadata.insert(
444 HookEvent::ElicitationResult,
445 HookEventMetadata {
446 summary: "After a user responds to an MCP elicitation".to_string(),
447 description: "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".to_string(),
448 matcher_metadata: Some(MatcherMetadata {
449 field_to_match: "mcp_server_name".to_string(),
450 values: Vec::new(),
451 }),
452 },
453 );
454
455 metadata.insert(
456 HookEvent::ConfigChange,
457 HookEventMetadata {
458 summary: "When configuration files change during a session".to_string(),
459 description: "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".to_string(),
460 matcher_metadata: Some(MatcherMetadata {
461 field_to_match: "source".to_string(),
462 values: vec![
463 "user_settings".to_string(),
464 "project_settings".to_string(),
465 "local_settings".to_string(),
466 "policy_settings".to_string(),
467 "skills".to_string(),
468 ],
469 }),
470 },
471 );
472
473 metadata.insert(
474 HookEvent::InstructionsLoaded,
475 HookEventMetadata {
476 summary: "When an instruction file (CLAUDE.md or rule) is loaded".to_string(),
477 description: "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.".to_string(),
478 matcher_metadata: Some(MatcherMetadata {
479 field_to_match: "load_reason".to_string(),
480 values: vec![
481 "session_start".to_string(),
482 "nested_traversal".to_string(),
483 "path_glob_match".to_string(),
484 "include".to_string(),
485 "compact".to_string(),
486 ],
487 }),
488 },
489 );
490
491 metadata.insert(
492 HookEvent::WorktreeCreate,
493 HookEventMetadata {
494 summary: "Create an isolated worktree for VCS-agnostic isolation".to_string(),
495 description: "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".to_string(),
496 matcher_metadata: None,
497 },
498 );
499
500 metadata.insert(
501 HookEvent::WorktreeRemove,
502 HookEventMetadata {
503 summary: "Remove a previously created worktree".to_string(),
504 description: "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".to_string(),
505 matcher_metadata: None,
506 },
507 );
508
509 metadata.insert(
510 HookEvent::CwdChanged,
511 HookEventMetadata {
512 summary: "After the working directory changes".to_string(),
513 description: "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".to_string(),
514 matcher_metadata: None,
515 },
516 );
517
518 metadata.insert(
519 HookEvent::FileChanged,
520 HookEventMetadata {
521 summary: "When a watched file changes".to_string(),
522 description: "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".to_string(),
523 matcher_metadata: None,
524 },
525 );
526
527 metadata
528}
529
530pub fn group_hooks_by_event_and_matcher(
532 tool_names: &[String],
533) -> HashMap<HookEvent, HashMap<String, Vec<IndividualHookConfig>>> {
534 let mut grouped: HashMap<HookEvent, HashMap<String, Vec<IndividualHookConfig>>> =
535 HashMap::new();
536
537 for event in HOOK_EVENTS {
539 grouped.insert(event.clone(), HashMap::new());
540 }
541
542 let metadata = get_hook_event_metadata(tool_names);
543
544 grouped
550}
551
552pub fn sort_matchers_by_priority(
554 matchers: &[String],
555 _hooks_by_event_and_matcher: &HashMap<HookEvent, HashMap<String, Vec<IndividualHookConfig>>>,
556 _event: &HookEvent,
557) -> Vec<String> {
558 let mut sorted = matchers.to_vec();
562 sorted.sort();
563 sorted
564}
565
566pub fn get_hooks_for_matcher(
568 hooks_by_event_and_matcher: &HashMap<HookEvent, HashMap<String, Vec<IndividualHookConfig>>>,
569 event: &HookEvent,
570 matcher: Option<&str>,
571) -> Vec<IndividualHookConfig> {
572 let matcher_key = matcher.unwrap_or("");
573 hooks_by_event_and_matcher
574 .get(event)
575 .and_then(|event_map| event_map.get(matcher_key))
576 .cloned()
577 .unwrap_or_default()
578}
579
580pub fn get_matcher_metadata(event: &HookEvent, tool_names: &[String]) -> Option<MatcherMetadata> {
582 let metadata = get_hook_event_metadata(tool_names);
583 metadata.get(event).and_then(|m| m.matcher_metadata.clone())
584}
585
586pub fn hook_source_description_display_string(source: &HookSource) -> String {
588 match source {
589 HookSource::UserSettings => "User settings (~/.claude/settings.json)".to_string(),
590 HookSource::ProjectSettings => "Project settings (.claude/settings.json)".to_string(),
591 HookSource::LocalSettings => "Local settings (.claude/settings.local.json)".to_string(),
592 HookSource::PluginHook => "Plugin hooks (~/.claude/plugins/*/hooks/hooks.json)".to_string(),
593 HookSource::SessionHook => "Session hooks (in-memory, temporary)".to_string(),
594 HookSource::BuiltinHook => {
595 "Built-in hook (registered internally by Claude Code)".to_string()
596 }
597 }
598}
599
600pub fn hook_source_header_display_string(source: &HookSource) -> String {
602 match source {
603 HookSource::UserSettings => "User Settings".to_string(),
604 HookSource::ProjectSettings => "Project Settings".to_string(),
605 HookSource::LocalSettings => "Local Settings".to_string(),
606 HookSource::PluginHook => "Plugin Hooks".to_string(),
607 HookSource::SessionHook => "Session Hooks".to_string(),
608 HookSource::BuiltinHook => "Built-in Hooks".to_string(),
609 }
610}
611
612pub fn hook_source_inline_display_string(source: &HookSource) -> String {
614 match source {
615 HookSource::UserSettings => "User".to_string(),
616 HookSource::ProjectSettings => "Project".to_string(),
617 HookSource::LocalSettings => "Local".to_string(),
618 HookSource::PluginHook => "Plugin".to_string(),
619 HookSource::SessionHook => "Session".to_string(),
620 HookSource::BuiltinHook => "Built-in".to_string(),
621 }
622}
623
624pub fn is_hook_equal(a: &HookCommand, b: &HookCommand) -> bool {
626 match (a, b) {
630 (
631 HookCommand::Command {
632 command: cmd_a,
633 shell: shell_a,
634 if_condition: if_a,
635 ..
636 },
637 HookCommand::Command {
638 command: cmd_b,
639 shell: shell_b,
640 if_condition: if_b,
641 ..
642 },
643 ) => {
644 cmd_a == cmd_b
645 && (shell_a.clone().unwrap_or_else(|| "bash".to_string())
646 == shell_b.clone().unwrap_or_else(|| "bash".to_string()))
647 && (if_a.clone().unwrap_or_default() == if_b.clone().unwrap_or_default())
648 }
649 (
650 HookCommand::Prompt {
651 prompt: p_a,
652 if_condition: if_a,
653 ..
654 },
655 HookCommand::Prompt {
656 prompt: p_b,
657 if_condition: if_b,
658 ..
659 },
660 ) => p_a == p_b && (if_a.clone().unwrap_or_default() == if_b.clone().unwrap_or_default()),
661 (
662 HookCommand::Agent {
663 prompt: p_a,
664 if_condition: if_a,
665 ..
666 },
667 HookCommand::Agent {
668 prompt: p_b,
669 if_condition: if_b,
670 ..
671 },
672 ) => p_a == p_b && (if_a.clone().unwrap_or_default() == if_b.clone().unwrap_or_default()),
673 (
674 HookCommand::Http {
675 url: u_a,
676 if_condition: if_a,
677 ..
678 },
679 HookCommand::Http {
680 url: u_b,
681 if_condition: if_b,
682 ..
683 },
684 ) => u_a == u_b && (if_a.clone().unwrap_or_default() == if_b.clone().unwrap_or_default()),
685 _ => false,
686 }
687}