Skip to main content

codex_hooks/
declarations.rs

1use codex_plugin::PluginHookSource;
2use codex_protocol::protocol::HookEventName;
3
4/// Minimal declaration metadata for one bundled plugin hook handler.
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct PluginHookDeclaration {
7    pub key: String,
8    pub event_name: HookEventName,
9}
10
11/// Return the hook handlers declared by plugin bundles without projecting live runtime state.
12pub fn plugin_hook_declarations(hook_sources: &[PluginHookSource]) -> Vec<PluginHookDeclaration> {
13    let mut declarations = Vec::new();
14
15    for source in hook_sources {
16        let key_source = plugin_hook_key_source(
17            source.plugin_id.as_key().as_str(),
18            source.source_relative_path.as_str(),
19        );
20        for (event_name, groups) in source.hooks.clone().into_matcher_groups() {
21            for (group_index, group) in groups.iter().enumerate() {
22                for (handler_index, _) in group.hooks.iter().enumerate() {
23                    declarations.push(PluginHookDeclaration {
24                        key: crate::hook_key(&key_source, event_name, group_index, handler_index),
25                        event_name,
26                    });
27                }
28            }
29        }
30    }
31
32    declarations
33}
34
35pub(crate) fn plugin_hook_key_source(plugin_id: &str, source_relative_path: &str) -> String {
36    format!("{plugin_id}:{source_relative_path}")
37}
38
39#[cfg(test)]
40mod tests {
41    use codex_config::HookEventsToml;
42    use codex_config::HookHandlerConfig;
43    use codex_config::MatcherGroup;
44    use codex_plugin::PluginId;
45    use codex_utils_absolute_path::test_support::PathBufExt;
46    use codex_utils_absolute_path::test_support::test_path_buf;
47    use pretty_assertions::assert_eq;
48
49    use super::*;
50
51    #[test]
52    fn lists_declared_plugin_handlers_with_persisted_hook_keys() {
53        let plugin_root = test_path_buf("/tmp/plugin").abs();
54        let source_path = plugin_root.join("hooks/hooks.json");
55        let declarations = plugin_hook_declarations(&[PluginHookSource {
56            plugin_id: PluginId::parse("demo@test").expect("plugin id"),
57            plugin_root: plugin_root.clone(),
58            plugin_data_root: plugin_root.join("data"),
59            source_path,
60            source_relative_path: "hooks/hooks.json".to_string(),
61            hooks: HookEventsToml {
62                pre_tool_use: vec![MatcherGroup {
63                    matcher: None,
64                    hooks: vec![
65                        HookHandlerConfig::Prompt {},
66                        HookHandlerConfig::Command {
67                            command: "echo hi".to_string(),
68                            command_windows: None,
69                            timeout_sec: None,
70                            r#async: false,
71                            status_message: None,
72                            additional_context_limit: None,
73                        },
74                    ],
75                }],
76                session_start: vec![MatcherGroup {
77                    matcher: None,
78                    hooks: vec![HookHandlerConfig::Agent {}],
79                }],
80                ..Default::default()
81            },
82        }]);
83
84        assert_eq!(
85            declarations,
86            vec![
87                PluginHookDeclaration {
88                    key: "demo@test:hooks/hooks.json:pre_tool_use:0:0".to_string(),
89                    event_name: HookEventName::PreToolUse,
90                },
91                PluginHookDeclaration {
92                    key: "demo@test:hooks/hooks.json:pre_tool_use:0:1".to_string(),
93                    event_name: HookEventName::PreToolUse,
94                },
95                PluginHookDeclaration {
96                    key: "demo@test:hooks/hooks.json:session_start:0:0".to_string(),
97                    event_name: HookEventName::SessionStart,
98                },
99            ]
100        );
101    }
102}