Skip to main content

codex_hooks/
registry.rs

1use codex_config::ConfigLayerStack;
2use codex_plugin::PluginHookSource;
3use tokio::process::Command;
4
5use crate::engine::ClaudeHooksEngine;
6use crate::engine::CommandShell;
7use crate::engine::HookListEntry;
8use crate::events::compact::PostCompactRequest;
9use crate::events::compact::PreCompactOutcome;
10use crate::events::compact::PreCompactRequest;
11use crate::events::compact::StatelessHookOutcome;
12use crate::events::permission_request::PermissionRequestOutcome;
13use crate::events::permission_request::PermissionRequestRequest;
14use crate::events::post_tool_use::PostToolUseOutcome;
15use crate::events::post_tool_use::PostToolUseRequest;
16use crate::events::pre_tool_use::PreToolUseOutcome;
17use crate::events::pre_tool_use::PreToolUseRequest;
18use crate::events::session_end::SessionEndOutcome;
19use crate::events::session_end::SessionEndRequest;
20use crate::events::session_start::SessionStartOutcome;
21use crate::events::session_start::SessionStartRequest;
22use crate::events::stop::StopOutcome;
23use crate::events::stop::StopRequest;
24use crate::events::user_prompt_submit::UserPromptSubmitOutcome;
25use crate::events::user_prompt_submit::UserPromptSubmitRequest;
26use crate::types::Hook;
27use crate::types::HookEvent;
28use crate::types::HookPayload;
29use crate::types::HookResponse;
30
31#[derive(Default, Clone)]
32pub struct HooksConfig {
33    pub legacy_notify_argv: Option<Vec<String>>,
34    pub feature_enabled: bool,
35    pub bypass_hook_trust: bool,
36    pub config_layer_stack: Option<ConfigLayerStack>,
37    pub plugin_hook_sources: Vec<PluginHookSource>,
38    pub plugin_hook_load_warnings: Vec<String>,
39    pub shell_program: Option<String>,
40    pub shell_args: Vec<String>,
41}
42
43#[derive(Debug, Clone, Default, PartialEq, Eq)]
44pub struct HookListOutcome {
45    pub hooks: Vec<HookListEntry>,
46    pub warnings: Vec<String>,
47}
48
49#[derive(Clone)]
50pub struct Hooks {
51    after_agent: Vec<Hook>,
52    engine: ClaudeHooksEngine,
53}
54
55impl Default for Hooks {
56    fn default() -> Self {
57        Self::new(HooksConfig::default())
58    }
59}
60
61impl Hooks {
62    pub fn new(config: HooksConfig) -> Self {
63        let after_agent = config
64            .legacy_notify_argv
65            .filter(|argv| !argv.is_empty() && !argv[0].is_empty())
66            .map(crate::notify_hook)
67            .into_iter()
68            .collect();
69        let engine = ClaudeHooksEngine::new(
70            config.feature_enabled,
71            config.bypass_hook_trust,
72            config.config_layer_stack.as_ref(),
73            config.plugin_hook_sources,
74            config.plugin_hook_load_warnings,
75            CommandShell {
76                program: config.shell_program.unwrap_or_default(),
77                args: config.shell_args,
78            },
79        );
80        Self {
81            after_agent,
82            engine,
83        }
84    }
85
86    pub fn startup_warnings(&self) -> &[String] {
87        self.engine.warnings()
88    }
89
90    fn hooks_for_event(&self, hook_event: &HookEvent) -> &[Hook] {
91        match hook_event {
92            HookEvent::AfterAgent { .. } => &self.after_agent,
93        }
94    }
95
96    pub async fn dispatch(&self, hook_payload: HookPayload) -> Vec<HookResponse> {
97        let hooks = self.hooks_for_event(&hook_payload.hook_event);
98        let mut outcomes = Vec::with_capacity(hooks.len());
99        for hook in hooks {
100            let outcome = hook.execute(&hook_payload).await;
101            let should_abort_operation = outcome.result.should_abort_operation();
102            outcomes.push(outcome);
103            if should_abort_operation {
104                break;
105            }
106        }
107
108        outcomes
109    }
110
111    pub fn preview_session_start(
112        &self,
113        request: &SessionStartRequest,
114    ) -> Vec<codex_protocol::protocol::HookRunSummary> {
115        self.engine.preview_session_start(request)
116    }
117
118    pub fn preview_pre_tool_use(
119        &self,
120        request: &PreToolUseRequest,
121    ) -> Vec<codex_protocol::protocol::HookRunSummary> {
122        self.engine.preview_pre_tool_use(request)
123    }
124
125    pub fn preview_permission_request(
126        &self,
127        request: &PermissionRequestRequest,
128    ) -> Vec<codex_protocol::protocol::HookRunSummary> {
129        self.engine.preview_permission_request(request)
130    }
131
132    pub fn preview_post_tool_use(
133        &self,
134        request: &PostToolUseRequest,
135    ) -> Vec<codex_protocol::protocol::HookRunSummary> {
136        self.engine.preview_post_tool_use(request)
137    }
138
139    pub async fn run_session_start(
140        &self,
141        request: SessionStartRequest,
142        turn_id: Option<String>,
143    ) -> SessionStartOutcome {
144        self.engine.run_session_start(request, turn_id).await
145    }
146
147    pub async fn run_pre_tool_use(&self, request: PreToolUseRequest) -> PreToolUseOutcome {
148        self.engine.run_pre_tool_use(request).await
149    }
150
151    pub async fn run_permission_request(
152        &self,
153        request: PermissionRequestRequest,
154    ) -> PermissionRequestOutcome {
155        self.engine.run_permission_request(request).await
156    }
157
158    pub async fn run_post_tool_use(&self, request: PostToolUseRequest) -> PostToolUseOutcome {
159        self.engine.run_post_tool_use(request).await
160    }
161
162    pub fn preview_pre_compact(
163        &self,
164        request: &PreCompactRequest,
165    ) -> Vec<codex_protocol::protocol::HookRunSummary> {
166        self.engine.preview_pre_compact(request)
167    }
168
169    pub async fn run_pre_compact(&self, request: PreCompactRequest) -> PreCompactOutcome {
170        self.engine.run_pre_compact(request).await
171    }
172
173    pub fn preview_post_compact(
174        &self,
175        request: &PostCompactRequest,
176    ) -> Vec<codex_protocol::protocol::HookRunSummary> {
177        self.engine.preview_post_compact(request)
178    }
179
180    pub async fn run_post_compact(&self, request: PostCompactRequest) -> StatelessHookOutcome {
181        self.engine.run_post_compact(request).await
182    }
183
184    pub fn preview_user_prompt_submit(
185        &self,
186        request: &UserPromptSubmitRequest,
187    ) -> Vec<codex_protocol::protocol::HookRunSummary> {
188        self.engine.preview_user_prompt_submit(request)
189    }
190
191    pub async fn run_user_prompt_submit(
192        &self,
193        request: UserPromptSubmitRequest,
194    ) -> UserPromptSubmitOutcome {
195        self.engine.run_user_prompt_submit(request).await
196    }
197
198    pub fn preview_stop(
199        &self,
200        request: &StopRequest,
201    ) -> Vec<codex_protocol::protocol::HookRunSummary> {
202        self.engine.preview_stop(request)
203    }
204
205    pub async fn run_stop(&self, request: StopRequest) -> StopOutcome {
206        self.engine.run_stop(request).await
207    }
208
209    pub fn preview_session_end(&self) -> Vec<codex_protocol::protocol::HookRunSummary> {
210        self.engine.preview_session_end()
211    }
212
213    pub async fn run_session_end(&self, request: SessionEndRequest) -> SessionEndOutcome {
214        self.engine.run_session_end(request).await
215    }
216}
217
218pub fn list_hooks(config: HooksConfig) -> HookListOutcome {
219    if !config.feature_enabled {
220        return HookListOutcome::default();
221    }
222
223    let discovered = crate::engine::discovery::discover_handlers(
224        config.config_layer_stack.as_ref(),
225        config.plugin_hook_sources,
226        config.plugin_hook_load_warnings,
227        config.bypass_hook_trust,
228    );
229    HookListOutcome {
230        hooks: discovered.hook_entries,
231        warnings: discovered.warnings,
232    }
233}
234
235pub fn command_from_argv(argv: &[String]) -> Option<Command> {
236    let (program, args) = argv.split_first()?;
237    if program.is_empty() {
238        return None;
239    }
240    let mut command = Command::new(program);
241    command.args(args);
242    Some(command)
243}