1use std::io::Write;
8
9use serde::{Deserialize, Serialize};
10use tracing::{Level, instrument};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
18#[serde(rename_all = "lowercase")]
19pub enum Effect {
20 Allow,
22 Ask,
24 Deny,
26}
27
28#[derive(Debug, Clone, Serialize, PartialEq)]
30#[serde(rename_all = "camelCase")]
31pub struct PreToolUseOutput {
32 pub hook_event_name: &'static str,
33 #[serde(skip_serializing_if = "Option::is_none")]
34 pub permission_decision: Option<Effect>,
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub permission_decision_reason: Option<String>,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub updated_input: Option<serde_json::Value>,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub additional_context: Option<String>,
41}
42
43#[derive(Debug, Clone, Serialize, PartialEq)]
45#[serde(rename_all = "lowercase")]
46pub enum PermissionBehavior {
47 Allow,
48 Deny,
49}
50
51#[derive(Debug, Clone, Serialize, PartialEq)]
53#[serde(rename_all = "camelCase")]
54pub struct PermissionDecision {
55 pub behavior: PermissionBehavior,
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub updated_input: Option<serde_json::Value>,
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub message: Option<String>,
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub interrupt: Option<bool>,
62}
63
64#[derive(Debug, Clone, Serialize, PartialEq)]
66#[serde(rename_all = "camelCase")]
67pub struct PermissionRequestOutput {
68 pub hook_event_name: &'static str,
69 pub decision: PermissionDecision,
70}
71
72#[derive(Debug, Clone, Serialize, PartialEq)]
74#[serde(rename_all = "camelCase")]
75pub struct SessionStartOutput {
76 pub hook_event_name: &'static str,
77 #[serde(skip_serializing_if = "Option::is_none")]
78 pub additional_context: Option<String>,
79}
80
81#[derive(Debug, Clone, Serialize, PartialEq)]
83#[serde(rename_all = "camelCase")]
84pub struct PostToolUseOutput {
85 pub hook_event_name: &'static str,
86 #[serde(skip_serializing_if = "Option::is_none")]
87 pub additional_context: Option<String>,
88}
89
90#[derive(Debug, Clone, Serialize, PartialEq)]
92#[serde(untagged)]
93pub enum HookSpecificOutput {
94 PreToolUse(PreToolUseOutput),
95 PostToolUse(PostToolUseOutput),
96 PermissionRequest(PermissionRequestOutput),
97 SessionStart(SessionStartOutput),
98}
99
100#[derive(Debug, Clone, Serialize, PartialEq)]
102#[serde(rename_all = "camelCase")]
103pub struct HookOutput {
104 #[serde(rename = "continue")]
105 pub should_continue: bool,
106 #[serde(skip_serializing_if = "Option::is_none")]
107 pub hook_specific_output: Option<HookSpecificOutput>,
108}
109
110impl HookOutput {
111 fn pretooluse_output(
113 decision: Effect,
114 reason: Option<String>,
115 context: Option<String>,
116 updated_input: Option<serde_json::Value>,
117 ) -> Self {
118 Self {
119 should_continue: true,
120 hook_specific_output: Some(HookSpecificOutput::PreToolUse(PreToolUseOutput {
121 hook_event_name: "PreToolUse",
122 permission_decision: Some(decision),
123 permission_decision_reason: reason,
124 updated_input,
125 additional_context: context,
126 })),
127 }
128 }
129
130 #[instrument(level = Level::TRACE)]
132 pub fn allow(reason: Option<String>, context: Option<String>) -> Self {
133 Self::pretooluse_output(Effect::Allow, reason, context, None)
134 }
135
136 #[instrument(level = Level::TRACE)]
138 pub fn deny(reason: String, context: Option<String>) -> Self {
139 Self::pretooluse_output(Effect::Deny, Some(reason), context, None)
140 }
141
142 #[instrument(level = Level::TRACE)]
144 pub fn ask(reason: Option<String>, context: Option<String>) -> Self {
145 Self::pretooluse_output(Effect::Ask, reason, context, None)
146 }
147
148 #[instrument(level = Level::TRACE)]
150 pub fn approve_permission(updated_input: Option<serde_json::Value>) -> Self {
151 Self {
152 should_continue: true,
153 hook_specific_output: Some(HookSpecificOutput::PermissionRequest(
154 PermissionRequestOutput {
155 hook_event_name: "PermissionRequest",
156 decision: PermissionDecision {
157 behavior: PermissionBehavior::Allow,
158 updated_input,
159 message: None,
160 interrupt: None,
161 },
162 },
163 )),
164 }
165 }
166
167 #[instrument(level = Level::TRACE)]
169 pub fn deny_permission(message: String, interrupt: bool) -> Self {
170 Self {
171 should_continue: true,
172 hook_specific_output: Some(HookSpecificOutput::PermissionRequest(
173 PermissionRequestOutput {
174 hook_event_name: "PermissionRequest",
175 decision: PermissionDecision {
176 behavior: PermissionBehavior::Deny,
177 updated_input: None,
178 message: Some(message),
179 interrupt: Some(interrupt),
180 },
181 },
182 )),
183 }
184 }
185
186 #[instrument(level = Level::TRACE, skip(self))]
189 pub fn set_updated_input(&mut self, updated_input: serde_json::Value) {
190 if let Some(HookSpecificOutput::PreToolUse(ref mut pre)) = self.hook_specific_output {
191 pre.updated_input = Some(updated_input);
192 }
193 }
194
195 #[instrument(level = Level::TRACE)]
197 pub fn session_start(additional_context: Option<String>) -> Self {
198 Self {
199 should_continue: true,
200 hook_specific_output: Some(HookSpecificOutput::SessionStart(SessionStartOutput {
201 hook_event_name: "SessionStart",
202 additional_context,
203 })),
204 }
205 }
206
207 #[instrument(level = Level::TRACE)]
209 pub fn post_tool_use(additional_context: Option<String>) -> Self {
210 match additional_context {
211 Some(ctx) => Self {
212 should_continue: true,
213 hook_specific_output: Some(HookSpecificOutput::PostToolUse(PostToolUseOutput {
214 hook_event_name: "PostToolUse",
215 additional_context: Some(ctx),
216 })),
217 },
218 None => Self::continue_execution(),
219 }
220 }
221
222 #[instrument(level = Level::TRACE)]
224 pub fn continue_execution() -> Self {
225 Self {
226 should_continue: true,
227 hook_specific_output: None,
228 }
229 }
230
231 #[instrument(level = Level::TRACE, skip(self, writer))]
233 pub fn write_to(&self, mut writer: impl Write) -> anyhow::Result<()> {
234 serde_json::to_writer(&mut writer, self)?;
235 writeln!(writer)?;
236 Ok(())
237 }
238
239 #[instrument(level = Level::TRACE, skip(self))]
241 pub fn write_stdout(&self) -> anyhow::Result<()> {
242 self.write_to(std::io::stdout().lock())
243 }
244
245 pub fn effect(&self) -> Option<Effect> {
247 match &self.hook_specific_output {
248 Some(HookSpecificOutput::PreToolUse(pre)) => pre.permission_decision,
249 _ => None,
250 }
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn test_output_allow() {
260 let output = HookOutput::allow(Some("Safe command".into()), None);
261 let mut buf = Vec::new();
262 output.write_to(&mut buf).unwrap();
263
264 let json: serde_json::Value = serde_json::from_slice(&buf).unwrap();
265 assert_eq!(json["hookSpecificOutput"]["permissionDecision"], "allow");
266 assert_eq!(
267 json["hookSpecificOutput"]["permissionDecisionReason"],
268 "Safe command"
269 );
270 }
271
272 #[test]
273 fn test_output_deny() {
274 let output = HookOutput::deny("Dangerous command".into(), None);
275 let mut buf = Vec::new();
276 output.write_to(&mut buf).unwrap();
277
278 let json: serde_json::Value = serde_json::from_slice(&buf).unwrap();
279 assert_eq!(json["hookSpecificOutput"]["permissionDecision"], "deny");
280 assert_eq!(
281 json["hookSpecificOutput"]["permissionDecisionReason"],
282 "Dangerous command"
283 );
284 }
285
286 #[test]
287 fn test_output_ask() {
288 let output = HookOutput::ask(None, None);
289 let mut buf = Vec::new();
290 output.write_to(&mut buf).unwrap();
291
292 let json: serde_json::Value = serde_json::from_slice(&buf).unwrap();
293 assert_eq!(json["hookSpecificOutput"]["permissionDecision"], "ask");
294 assert!(json["hookSpecificOutput"]["permissionDecisionReason"].is_null());
295 }
296
297 #[test]
298 fn test_approve_permission() {
299 let output = HookOutput::approve_permission(None);
300 let mut buf = Vec::new();
301 output.write_to(&mut buf).unwrap();
302
303 let json: serde_json::Value = serde_json::from_slice(&buf).unwrap();
304 assert_eq!(
305 json["hookSpecificOutput"]["hookEventName"],
306 "PermissionRequest"
307 );
308 assert_eq!(json["hookSpecificOutput"]["decision"]["behavior"], "allow");
309 assert!(json["hookSpecificOutput"]["decision"]["updatedInput"].is_null());
310 }
311
312 #[test]
313 fn test_approve_permission_with_updated_input() {
314 let updated = serde_json::json!({"command": "ls -la"});
315 let output = HookOutput::approve_permission(Some(updated.clone()));
316 let mut buf = Vec::new();
317 output.write_to(&mut buf).unwrap();
318
319 let json: serde_json::Value = serde_json::from_slice(&buf).unwrap();
320 assert_eq!(
321 json["hookSpecificOutput"]["hookEventName"],
322 "PermissionRequest"
323 );
324 assert_eq!(json["hookSpecificOutput"]["decision"]["behavior"], "allow");
325 assert_eq!(
326 json["hookSpecificOutput"]["decision"]["updatedInput"],
327 updated
328 );
329 }
330
331 #[test]
332 fn test_deny_permission() {
333 let output = HookOutput::deny_permission("Not allowed".into(), true);
334 let mut buf = Vec::new();
335 output.write_to(&mut buf).unwrap();
336
337 let json: serde_json::Value = serde_json::from_slice(&buf).unwrap();
338 assert_eq!(
339 json["hookSpecificOutput"]["hookEventName"],
340 "PermissionRequest"
341 );
342 assert_eq!(json["hookSpecificOutput"]["decision"]["behavior"], "deny");
343 assert_eq!(
344 json["hookSpecificOutput"]["decision"]["message"],
345 "Not allowed"
346 );
347 assert_eq!(json["hookSpecificOutput"]["decision"]["interrupt"], true);
348 }
349
350 #[test]
351 fn test_deny_permission_no_interrupt() {
352 let output = HookOutput::deny_permission("Try again".into(), false);
353 let mut buf = Vec::new();
354 output.write_to(&mut buf).unwrap();
355
356 let json: serde_json::Value = serde_json::from_slice(&buf).unwrap();
357 assert_eq!(json["hookSpecificOutput"]["decision"]["behavior"], "deny");
358 assert_eq!(json["hookSpecificOutput"]["decision"]["interrupt"], false);
359 }
360
361 #[test]
362 fn test_effect_extraction() {
363 assert_eq!(HookOutput::allow(None, None).effect(), Some(Effect::Allow));
364 assert_eq!(
365 HookOutput::deny("x".into(), None).effect(),
366 Some(Effect::Deny)
367 );
368 assert_eq!(HookOutput::ask(None, None).effect(), Some(Effect::Ask));
369 assert_eq!(HookOutput::continue_execution().effect(), None);
370 }
371}