Skip to main content

claude_code_rs/types/
hooks.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::future::Future;
4use std::pin::Pin;
5use std::sync::Arc;
6
7/// Hook events that can be intercepted.
8#[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/// Matcher for which tool/event a hook applies to.
42#[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/// Input for a preToolUse hook.
49#[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/// Input for a postToolUse hook.
58#[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/// Input for a notification hook.
69#[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/// Input for a stop hook.
78#[derive(Debug, Clone, Default, Serialize, Deserialize)]
79pub struct StopInput {
80    #[serde(default)]
81    pub reason: Option<String>,
82}
83
84/// Discriminated hook input passed to callbacks.
85#[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/// Output from a hook callback.
96#[derive(Debug, Clone, Serialize, Deserialize, Default)]
97pub struct HookOutput {
98    /// If set, blocks the tool use with this reason.
99    #[serde(default, skip_serializing_if = "Option::is_none")]
100    pub decision: Option<HookDecision>,
101    /// Optional reason/message.
102    #[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/// A registered hook definition.
158#[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
175/// Async hook callback type.
176pub type HookCallback =
177    Arc<dyn Fn(HookInput) -> Pin<Box<dyn Future<Output = HookOutput> + Send>> + Send + Sync>;
178
179/// Helper to create a HookCallback from an async closure.
180pub 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}