1use std::collections::BTreeMap;
2use std::path::Path;
3use std::path::PathBuf;
4
5use codex_protocol::protocol::HookEventName;
6use schemars::JsonSchema;
7use serde::Deserialize;
8use serde::Serialize;
9
10#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
11#[serde(deny_unknown_fields)]
12pub struct HooksFile {
13 #[serde(default, skip_serializing_if = "Option::is_none")]
14 pub description: Option<String>,
15 #[serde(default)]
16 pub hooks: HookEventsToml,
17}
18
19#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
20pub struct HooksToml {
21 #[serde(flatten)]
22 pub events: HookEventsToml,
23 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
24 pub state: BTreeMap<String, HookStateToml>,
25}
26
27#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
28pub struct HookStateToml {
29 #[serde(default, skip_serializing_if = "Option::is_none")]
30 pub enabled: Option<bool>,
31 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub trusted_hash: Option<String>,
33}
34
35#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
36pub struct HookEventsToml {
37 #[serde(rename = "PreToolUse", default)]
38 pub pre_tool_use: Vec<MatcherGroup>,
39 #[serde(rename = "PermissionRequest", default)]
40 pub permission_request: Vec<MatcherGroup>,
41 #[serde(rename = "PostToolUse", default)]
42 pub post_tool_use: Vec<MatcherGroup>,
43 #[serde(rename = "PreCompact", default)]
44 pub pre_compact: Vec<MatcherGroup>,
45 #[serde(rename = "PostCompact", default)]
46 pub post_compact: Vec<MatcherGroup>,
47 #[serde(rename = "SessionStart", default)]
48 pub session_start: Vec<MatcherGroup>,
49 #[serde(rename = "SessionEnd", default)]
50 pub session_end: Vec<MatcherGroup>,
51 #[serde(rename = "UserPromptSubmit", default)]
52 pub user_prompt_submit: Vec<MatcherGroup>,
53 #[serde(rename = "SubagentStart", default)]
54 pub subagent_start: Vec<MatcherGroup>,
55 #[serde(rename = "SubagentStop", default)]
56 pub subagent_stop: Vec<MatcherGroup>,
57 #[serde(rename = "Stop", default)]
58 pub stop: Vec<MatcherGroup>,
59}
60
61impl HookEventsToml {
62 pub fn is_empty(&self) -> bool {
63 let Self {
64 pre_tool_use,
65 permission_request,
66 post_tool_use,
67 pre_compact,
68 post_compact,
69 session_start,
70 session_end,
71 user_prompt_submit,
72 subagent_start,
73 subagent_stop,
74 stop,
75 } = self;
76 pre_tool_use.is_empty()
77 && permission_request.is_empty()
78 && post_tool_use.is_empty()
79 && pre_compact.is_empty()
80 && post_compact.is_empty()
81 && session_start.is_empty()
82 && session_end.is_empty()
83 && user_prompt_submit.is_empty()
84 && subagent_start.is_empty()
85 && subagent_stop.is_empty()
86 && stop.is_empty()
87 }
88
89 pub fn handler_count(&self) -> usize {
90 let Self {
91 pre_tool_use,
92 permission_request,
93 post_tool_use,
94 pre_compact,
95 post_compact,
96 session_start,
97 session_end,
98 user_prompt_submit,
99 subagent_start,
100 subagent_stop,
101 stop,
102 } = self;
103 [
104 pre_tool_use,
105 permission_request,
106 post_tool_use,
107 pre_compact,
108 post_compact,
109 session_start,
110 session_end,
111 user_prompt_submit,
112 subagent_start,
113 subagent_stop,
114 stop,
115 ]
116 .into_iter()
117 .flatten()
118 .map(|group| group.hooks.len())
119 .sum()
120 }
121
122 pub fn into_matcher_groups(self) -> [(HookEventName, Vec<MatcherGroup>); 11] {
123 [
124 (HookEventName::PreToolUse, self.pre_tool_use),
125 (HookEventName::PermissionRequest, self.permission_request),
126 (HookEventName::PostToolUse, self.post_tool_use),
127 (HookEventName::PreCompact, self.pre_compact),
128 (HookEventName::PostCompact, self.post_compact),
129 (HookEventName::SessionStart, self.session_start),
130 (HookEventName::SessionEnd, self.session_end),
131 (HookEventName::UserPromptSubmit, self.user_prompt_submit),
132 (HookEventName::SubagentStart, self.subagent_start),
133 (HookEventName::SubagentStop, self.subagent_stop),
134 (HookEventName::Stop, self.stop),
135 ]
136 }
137}
138
139#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
140pub struct MatcherGroup {
141 #[serde(default)]
142 pub matcher: Option<String>,
143 #[serde(default)]
144 pub hooks: Vec<HookHandlerConfig>,
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
148#[serde(tag = "type")]
149pub enum HookHandlerConfig {
150 #[serde(rename = "command")]
151 Command {
152 command: String,
153 #[serde(default, rename = "commandWindows", alias = "command_windows")]
154 command_windows: Option<String>,
155 #[serde(default, rename = "timeout")]
156 timeout_sec: Option<u64>,
157 #[serde(default)]
158 r#async: bool,
159 #[serde(default, rename = "statusMessage")]
160 status_message: Option<String>,
161 #[serde(
166 default,
167 rename = "additionalContextLimit",
168 skip_serializing_if = "Option::is_none"
169 )]
170 additional_context_limit: Option<usize>,
171 },
172 #[serde(rename = "prompt")]
173 Prompt {},
174 #[serde(rename = "agent")]
175 Agent {},
176}
177
178#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
179pub struct ManagedHooksRequirementsToml {
180 pub managed_dir: Option<PathBuf>,
181 pub windows_managed_dir: Option<PathBuf>,
182 #[serde(flatten)]
183 pub hooks: HookEventsToml,
184}
185
186impl ManagedHooksRequirementsToml {
187 pub fn is_empty(&self) -> bool {
188 let Self {
189 managed_dir,
190 windows_managed_dir,
191 hooks,
192 } = self;
193 managed_dir.is_none() && windows_managed_dir.is_none() && hooks.is_empty()
194 }
195
196 pub fn handler_count(&self) -> usize {
197 self.hooks.handler_count()
198 }
199
200 pub fn managed_dir_for_current_platform(&self) -> Option<&Path> {
201 #[cfg(windows)]
202 {
203 self.windows_managed_dir.as_deref()
204 }
205
206 #[cfg(not(windows))]
207 {
208 self.managed_dir.as_deref()
209 }
210 }
211}
212
213#[cfg(test)]
214#[path = "hooks_tests.rs"]
215mod tests;