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)]
9pub enum HookEvent {
10    #[serde(rename = "preToolUse")]
11    PreToolUse,
12    #[serde(rename = "postToolUse")]
13    PostToolUse,
14    #[serde(rename = "notification")]
15    Notification,
16    #[serde(rename = "stop")]
17    Stop,
18    #[serde(rename = "subagentStop")]
19    SubagentStop,
20}
21
22/// Matcher for which tool/event a hook applies to.
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct HookMatcher {
25    #[serde(default, skip_serializing_if = "Option::is_none")]
26    pub tool_name: Option<String>,
27}
28
29/// Input for a preToolUse hook.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct PreToolUseInput {
32    pub tool_name: String,
33    pub tool_input: Value,
34}
35
36/// Input for a postToolUse hook.
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct PostToolUseInput {
39    pub tool_name: String,
40    pub tool_input: Value,
41    pub tool_output: Value,
42}
43
44/// Input for a notification hook.
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct NotificationInput {
47    pub title: String,
48    #[serde(default)]
49    pub message: Option<String>,
50}
51
52/// Input for a stop hook.
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct StopInput {
55    #[serde(default)]
56    pub reason: Option<String>,
57}
58
59/// Discriminated hook input passed to callbacks.
60#[derive(Debug, Clone, Serialize, Deserialize)]
61#[serde(tag = "type", rename_all = "camelCase")]
62pub enum HookInput {
63    PreToolUse(PreToolUseInput),
64    PostToolUse(PostToolUseInput),
65    Notification(NotificationInput),
66    Stop(StopInput),
67}
68
69/// Output from a hook callback.
70#[derive(Debug, Clone, Serialize, Deserialize, Default)]
71pub struct HookOutput {
72    /// If set, blocks the tool use with this reason.
73    #[serde(default, skip_serializing_if = "Option::is_none")]
74    pub decision: Option<HookDecision>,
75    /// Optional reason/message.
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    pub reason: Option<String>,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
81#[serde(rename_all = "lowercase")]
82pub enum HookDecision {
83    Approve,
84    Block,
85    Ignore,
86}
87
88impl HookOutput {
89    pub fn approve() -> Self {
90        Self {
91            decision: Some(HookDecision::Approve),
92            reason: None,
93        }
94    }
95
96    pub fn block(reason: impl Into<String>) -> Self {
97        Self {
98            decision: Some(HookDecision::Block),
99            reason: Some(reason.into()),
100        }
101    }
102
103    pub fn ignore() -> Self {
104        Self {
105            decision: Some(HookDecision::Ignore),
106            reason: None,
107        }
108    }
109}
110
111/// A registered hook definition.
112#[derive(Clone)]
113pub struct HookDefinition {
114    pub event: HookEvent,
115    pub matcher: HookMatcher,
116    pub callback: HookCallback,
117}
118
119impl std::fmt::Debug for HookDefinition {
120    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        f.debug_struct("HookDefinition")
122            .field("event", &self.event)
123            .field("matcher", &self.matcher)
124            .field("callback", &"<fn>")
125            .finish()
126    }
127}
128
129/// Async hook callback type.
130pub type HookCallback =
131    Arc<dyn Fn(HookInput) -> Pin<Box<dyn Future<Output = HookOutput> + Send>> + Send + Sync>;
132
133/// Helper to create a HookCallback from an async closure.
134pub fn hook_callback<F, Fut>(f: F) -> HookCallback
135where
136    F: Fn(HookInput) -> Fut + Send + Sync + 'static,
137    Fut: Future<Output = HookOutput> + Send + 'static,
138{
139    Arc::new(move |input| Box::pin(f(input)))
140}