1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::future::Future;
4use std::pin::Pin;
5use std::sync::Arc;
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
9#[non_exhaustive]
10pub enum HookEvent {
11 #[serde(rename = "preToolUse")]
12 PreToolUse,
13 #[serde(rename = "postToolUse")]
14 PostToolUse,
15 #[serde(rename = "notification")]
16 Notification,
17 #[serde(rename = "stop")]
18 Stop,
19 #[serde(rename = "subagentStop")]
20 SubagentStop,
21}
22
23impl std::fmt::Display for HookEvent {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 f.write_str(self.as_str())
26 }
27}
28
29impl HookEvent {
30 pub fn as_str(&self) -> &'static str {
31 match self {
32 Self::PreToolUse => "PreToolUse",
33 Self::PostToolUse => "PostToolUse",
34 Self::Notification => "Notification",
35 Self::Stop => "Stop",
36 Self::SubagentStop => "SubagentStop",
37 }
38 }
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct HookMatcher {
44 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub tool_name: Option<String>,
46}
47
48#[derive(Debug, Clone, Default, Serialize, Deserialize)]
50pub struct PreToolUseInput {
51 #[serde(default)]
52 pub tool_name: String,
53 #[serde(default)]
54 pub tool_input: Value,
55}
56
57#[derive(Debug, Clone, Default, Serialize, Deserialize)]
59pub struct PostToolUseInput {
60 #[serde(default)]
61 pub tool_name: String,
62 #[serde(default)]
63 pub tool_input: Value,
64 #[serde(default)]
65 pub tool_output: Value,
66}
67
68#[derive(Debug, Clone, Default, Serialize, Deserialize)]
70pub struct NotificationInput {
71 #[serde(default)]
72 pub title: String,
73 #[serde(default)]
74 pub message: Option<String>,
75}
76
77#[derive(Debug, Clone, Default, Serialize, Deserialize)]
79pub struct StopInput {
80 #[serde(default)]
81 pub reason: Option<String>,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
86#[serde(tag = "type", rename_all = "camelCase")]
87#[non_exhaustive]
88pub enum HookInput {
89 PreToolUse(PreToolUseInput),
90 PostToolUse(PostToolUseInput),
91 Notification(NotificationInput),
92 Stop(StopInput),
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize, Default)]
97pub struct HookOutput {
98 #[serde(default, skip_serializing_if = "Option::is_none")]
100 pub decision: Option<HookDecision>,
101 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub reason: Option<String>,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
107#[serde(rename_all = "lowercase")]
108#[non_exhaustive]
109pub enum HookDecision {
110 Approve,
111 Block,
112 Ignore,
113}
114
115impl std::fmt::Display for HookDecision {
116 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117 f.write_str(self.as_str())
118 }
119}
120
121impl HookDecision {
122 pub fn as_str(&self) -> &'static str {
123 match self {
124 Self::Approve => "approve",
125 Self::Block => "deny",
126 Self::Ignore => "ignore",
127 }
128 }
129}
130
131impl HookOutput {
132 #[must_use]
133 pub fn approve() -> Self {
134 Self {
135 decision: Some(HookDecision::Approve),
136 reason: None,
137 }
138 }
139
140 #[must_use]
141 pub fn block(reason: impl Into<String>) -> Self {
142 Self {
143 decision: Some(HookDecision::Block),
144 reason: Some(reason.into()),
145 }
146 }
147
148 #[must_use]
149 pub fn ignore() -> Self {
150 Self {
151 decision: Some(HookDecision::Ignore),
152 reason: None,
153 }
154 }
155}
156
157#[derive(Clone)]
159pub struct HookDefinition {
160 pub event: HookEvent,
161 pub matcher: HookMatcher,
162 pub callback: HookCallback,
163}
164
165impl std::fmt::Debug for HookDefinition {
166 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167 f.debug_struct("HookDefinition")
168 .field("event", &self.event)
169 .field("matcher", &self.matcher)
170 .field("callback", &"<fn>")
171 .finish()
172 }
173}
174
175pub type HookCallback =
177 Arc<dyn Fn(HookInput) -> Pin<Box<dyn Future<Output = HookOutput> + Send>> + Send + Sync>;
178
179pub fn hook_callback<F, Fut>(f: F) -> HookCallback
181where
182 F: Fn(HookInput) -> Fut + Send + Sync + 'static,
183 Fut: Future<Output = HookOutput> + Send + 'static,
184{
185 Arc::new(move |input| Box::pin(f(input)))
186}