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}