Skip to main content

ai_agent/types/
hooks.rs

1// Source: ~/claudecode/openclaudecode/src/types/hooks.ts
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6use crate::types::message::Message;
7
8/// Hook events enum matching HOOK_EVENTS from the SDK.
9pub type HookEvent = String;
10
11/// Hook input type from the SDK.
12pub type HookInput = HashMap<String, serde_json::Value>;
13
14/// Permission update type.
15pub type PermissionUpdate = HashMap<String, serde_json::Value>;
16
17/// Hook JSON output type.
18pub type HookJSONOutput = serde_json::Value;
19
20/// Async hook JSON output type.
21pub type AsyncHookJSONOutput = serde_json::Value;
22
23/// Sync hook JSON output type.
24pub type SyncHookJSONOutput = serde_json::Value;
25
26/// Check if a value is a valid HookEvent.
27pub fn is_hook_event(value: &str) -> bool {
28    // Hook events list from SDK
29    let events = [
30        "PreToolUse",
31        "UserPromptSubmit",
32        "SessionStart",
33        "Setup",
34        "SubagentStart",
35        "PostToolUse",
36        "PostToolUseFailure",
37        "PermissionDenied",
38        "Notification",
39        "PermissionRequest",
40        "Elicitation",
41        "ElicitationResult",
42        "CwdChanged",
43        "FileChanged",
44        "WorktreeCreate",
45    ];
46    events.contains(&value)
47}
48
49/// Prompt elicitation request.
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct PromptRequest {
52    /// Request id
53    pub prompt: String,
54    pub message: String,
55    pub options: Vec<PromptOption>,
56}
57
58/// An option in a prompt request.
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct PromptOption {
61    pub key: String,
62    pub label: String,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub description: Option<String>,
65}
66
67/// Response to a prompt request.
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct PromptResponse {
70    /// Request id
71    #[serde(rename = "prompt_response")]
72    pub prompt_response: String,
73    pub selected: String,
74}
75
76/// Sync hook response - whether to continue after hook.
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct SyncHookResponse {
79    /// Whether Claude should continue after hook (default: true)
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub continue_flag: Option<bool>,
82    /// Hide stdout from transcript (default: false)
83    #[serde(skip_serializing_if = "Option::is_none")]
84    #[serde(rename = "suppressOutput")]
85    pub suppress_output: Option<bool>,
86    /// Message shown when continue is false
87    #[serde(skip_serializing_if = "Option::is_none")]
88    #[serde(rename = "stopReason")]
89    pub stop_reason: Option<String>,
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub decision: Option<HookDecision>,
92    /// Explanation for the decision
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub reason: Option<String>,
95    /// Warning message shown to the user
96    #[serde(skip_serializing_if = "Option::is_none")]
97    #[serde(rename = "systemMessage")]
98    pub system_message: Option<String>,
99    /// Hook-specific output
100    #[serde(skip_serializing_if = "Option::is_none")]
101    #[serde(rename = "hookSpecificOutput")]
102    pub hook_specific_output: Option<HookSpecificOutput>,
103}
104
105/// Hook decision enum.
106#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
107#[serde(rename_all = "lowercase")]
108pub enum HookDecision {
109    Approve,
110    Block,
111}
112
113/// Hook-specific output discriminated by hook event name.
114#[derive(Debug, Clone, Serialize, Deserialize)]
115#[serde(tag = "hookEventName")]
116pub enum HookSpecificOutput {
117    #[serde(rename = "PreToolUse")]
118    PreToolUse {
119        #[serde(skip_serializing_if = "Option::is_none")]
120        #[serde(rename = "permissionDecision")]
121        permission_decision: Option<String>,
122        #[serde(skip_serializing_if = "Option::is_none")]
123        #[serde(rename = "permissionDecisionReason")]
124        permission_decision_reason: Option<String>,
125        #[serde(skip_serializing_if = "Option::is_none")]
126        #[serde(rename = "updatedInput")]
127        updated_input: Option<HashMap<String, serde_json::Value>>,
128        #[serde(skip_serializing_if = "Option::is_none")]
129        #[serde(rename = "additionalContext")]
130        additional_context: Option<String>,
131    },
132    #[serde(rename = "UserPromptSubmit")]
133    UserPromptSubmit {
134        #[serde(skip_serializing_if = "Option::is_none")]
135        #[serde(rename = "additionalContext")]
136        additional_context: Option<String>,
137    },
138    #[serde(rename = "SessionStart")]
139    SessionStart {
140        #[serde(skip_serializing_if = "Option::is_none")]
141        #[serde(rename = "additionalContext")]
142        additional_context: Option<String>,
143        #[serde(skip_serializing_if = "Option::is_none")]
144        #[serde(rename = "initialUserMessage")]
145        initial_user_message: Option<String>,
146        /// Absolute paths to watch for FileChanged hooks
147        #[serde(skip_serializing_if = "Option::is_none")]
148        #[serde(rename = "watchPaths")]
149        watch_paths: Option<Vec<String>>,
150    },
151    #[serde(rename = "Setup")]
152    Setup {
153        #[serde(skip_serializing_if = "Option::is_none")]
154        #[serde(rename = "additionalContext")]
155        additional_context: Option<String>,
156    },
157    #[serde(rename = "SubagentStart")]
158    SubagentStart {
159        #[serde(skip_serializing_if = "Option::is_none")]
160        #[serde(rename = "additionalContext")]
161        additional_context: Option<String>,
162    },
163    #[serde(rename = "PostToolUse")]
164    PostToolUse {
165        #[serde(skip_serializing_if = "Option::is_none")]
166        #[serde(rename = "additionalContext")]
167        additional_context: Option<String>,
168        /// Updates the output for MCP tools
169        #[serde(skip_serializing_if = "Option::is_none")]
170        #[serde(rename = "updatedMCPToolOutput")]
171        updated_mcp_tool_output: Option<serde_json::Value>,
172    },
173    #[serde(rename = "PostToolUseFailure")]
174    PostToolUseFailure {
175        #[serde(skip_serializing_if = "Option::is_none")]
176        #[serde(rename = "additionalContext")]
177        additional_context: Option<String>,
178    },
179    #[serde(rename = "PermissionDenied")]
180    PermissionDenied {
181        #[serde(skip_serializing_if = "Option::is_none")]
182        retry: Option<bool>,
183    },
184    #[serde(rename = "Notification")]
185    Notification {
186        #[serde(skip_serializing_if = "Option::is_none")]
187        #[serde(rename = "additionalContext")]
188        additional_context: Option<String>,
189    },
190    #[serde(rename = "PermissionRequest")]
191    PermissionRequest {
192        #[serde(flatten)]
193        decision: PermissionRequestDecision,
194    },
195    #[serde(rename = "Elicitation")]
196    Elicitation {
197        #[serde(skip_serializing_if = "Option::is_none")]
198        action: Option<ElicitationAction>,
199        #[serde(skip_serializing_if = "Option::is_none")]
200        content: Option<HashMap<String, serde_json::Value>>,
201    },
202    #[serde(rename = "ElicitationResult")]
203    ElicitationResult {
204        #[serde(skip_serializing_if = "Option::is_none")]
205        action: Option<ElicitationAction>,
206        #[serde(skip_serializing_if = "Option::is_none")]
207        content: Option<HashMap<String, serde_json::Value>>,
208    },
209    #[serde(rename = "CwdChanged")]
210    CwdChanged {
211        /// Absolute paths to watch for FileChanged hooks
212        #[serde(skip_serializing_if = "Option::is_none")]
213        #[serde(rename = "watchPaths")]
214        watch_paths: Option<Vec<String>>,
215    },
216    #[serde(rename = "FileChanged")]
217    FileChanged {
218        /// Absolute paths to watch for FileChanged hooks
219        #[serde(skip_serializing_if = "Option::is_none")]
220        #[serde(rename = "watchPaths")]
221        watch_paths: Option<Vec<String>>,
222    },
223    #[serde(rename = "WorktreeCreate")]
224    WorktreeCreate {
225        #[serde(rename = "worktreePath")]
226        worktree_path: String,
227    },
228}
229
230/// Permission request decision.
231#[derive(Debug, Clone, Serialize, Deserialize)]
232#[serde(tag = "behavior")]
233pub enum PermissionRequestDecision {
234    #[serde(rename = "allow")]
235    Allow {
236        #[serde(skip_serializing_if = "Option::is_none")]
237        #[serde(rename = "updatedInput")]
238        updated_input: Option<HashMap<String, serde_json::Value>>,
239        #[serde(skip_serializing_if = "Option::is_none")]
240        #[serde(rename = "updatedPermissions")]
241        updated_permissions: Option<Vec<PermissionUpdate>>,
242    },
243    #[serde(rename = "deny")]
244    Deny {
245        #[serde(skip_serializing_if = "Option::is_none")]
246        message: Option<String>,
247        #[serde(skip_serializing_if = "Option::is_none")]
248        interrupt: Option<bool>,
249    },
250}
251
252/// Elicitation action.
253#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
254#[serde(rename_all = "lowercase")]
255pub enum ElicitationAction {
256    Accept,
257    Decline,
258    Cancel,
259}
260
261/// Type guard to check if response is sync.
262pub fn is_sync_hook_json_output(json: &HookJSONOutput) -> bool {
263    !json.get("async").is_some_and(|v| v.as_bool() == Some(true))
264}
265
266/// Type guard to check if response is async.
267pub fn is_async_hook_json_output(json: &HookJSONOutput) -> bool {
268    json.get("async").is_some_and(|v| v.as_bool() == Some(true))
269}
270
271/// Context passed to callback hooks for state access.
272pub struct HookCallbackContext {
273    pub get_app_state: Box<dyn Fn() -> Box<dyn std::any::Any> + Send + Sync>,
274    pub update_attribution_state:
275        Box<dyn Fn(Box<dyn std::any::Any>) -> Box<dyn std::any::Any> + Send + Sync>,
276}
277
278/// Hook that is a callback.
279pub struct HookCallback {
280    pub callback_type: String, // "callback"
281    pub callback: Box<
282        dyn Fn(
283                HookInput,
284                Option<String>,                             // toolUseID
285                Option<tokio::sync::oneshot::Receiver<()>>, // abort signal
286                Option<usize>,                              // hookIndex
287                Option<HookCallbackContext>,
288            )
289                -> std::pin::Pin<Box<dyn std::future::Future<Output = HookJSONOutput> + Send>>
290            + Send
291            + Sync,
292    >,
293    /// Timeout in seconds for this hook
294    pub timeout: Option<u64>,
295    /// Internal hooks excluded from tengu_run_hook metrics
296    pub internal: Option<bool>,
297}
298
299/// Hook callback matcher.
300pub struct HookCallbackMatcher {
301    pub matcher: Option<String>,
302    pub hooks: Vec<HookCallback>,
303    pub plugin_name: Option<String>,
304}
305
306/// Hook progress message.
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct HookProgress {
309    #[serde(rename = "type")]
310    pub entry_type: String, // "hook_progress"
311    #[serde(rename = "hookEvent")]
312    pub hook_event: HookEvent,
313    #[serde(rename = "hookName")]
314    pub hook_name: String,
315    pub command: String,
316    #[serde(skip_serializing_if = "Option::is_none")]
317    #[serde(rename = "promptText")]
318    pub prompt_text: Option<String>,
319    #[serde(skip_serializing_if = "Option::is_none")]
320    #[serde(rename = "statusMessage")]
321    pub status_message: Option<String>,
322}
323
324/// Hook blocking error.
325#[derive(Debug, Clone, Serialize, Deserialize)]
326pub struct HookBlockingError {
327    #[serde(rename = "blockingError")]
328    pub blocking_error: String,
329    pub command: String,
330}
331
332/// Permission request result.
333#[derive(Debug, Clone, Serialize, Deserialize)]
334#[serde(tag = "behavior")]
335pub enum PermissionRequestResult {
336    #[serde(rename = "allow")]
337    Allow {
338        #[serde(skip_serializing_if = "Option::is_none")]
339        #[serde(rename = "updatedInput")]
340        updated_input: Option<HashMap<String, serde_json::Value>>,
341        #[serde(skip_serializing_if = "Option::is_none")]
342        #[serde(rename = "updatedPermissions")]
343        updated_permissions: Option<Vec<PermissionUpdate>>,
344    },
345    #[serde(rename = "deny")]
346    Deny {
347        #[serde(skip_serializing_if = "Option::is_none")]
348        message: Option<String>,
349        #[serde(skip_serializing_if = "Option::is_none")]
350        interrupt: Option<bool>,
351    },
352}
353
354/// Result from running a hook.
355#[derive(Debug, Clone, Serialize, Deserialize)]
356pub struct HookResult {
357    #[serde(skip_serializing_if = "Option::is_none")]
358    pub message: Option<Message>,
359    #[serde(skip_serializing_if = "Option::is_none")]
360    #[serde(rename = "systemMessage")]
361    pub system_message: Option<Message>,
362    #[serde(skip_serializing_if = "Option::is_none")]
363    #[serde(rename = "blockingError")]
364    pub blocking_error: Option<HookBlockingError>,
365    pub outcome: HookOutcome,
366    #[serde(skip_serializing_if = "Option::is_none")]
367    #[serde(rename = "preventContinuation")]
368    pub prevent_continuation: Option<bool>,
369    #[serde(skip_serializing_if = "Option::is_none")]
370    #[serde(rename = "stopReason")]
371    pub stop_reason: Option<String>,
372    #[serde(skip_serializing_if = "Option::is_none")]
373    #[serde(rename = "permissionBehavior")]
374    pub permission_behavior: Option<PermissionBehavior>,
375    #[serde(skip_serializing_if = "Option::is_none")]
376    #[serde(rename = "hookPermissionDecisionReason")]
377    pub hook_permission_decision_reason: Option<String>,
378    #[serde(skip_serializing_if = "Option::is_none")]
379    #[serde(rename = "additionalContext")]
380    pub additional_context: Option<String>,
381    #[serde(skip_serializing_if = "Option::is_none")]
382    #[serde(rename = "initialUserMessage")]
383    pub initial_user_message: Option<String>,
384    #[serde(skip_serializing_if = "Option::is_none")]
385    #[serde(rename = "updatedInput")]
386    pub updated_input: Option<HashMap<String, serde_json::Value>>,
387    #[serde(skip_serializing_if = "Option::is_none")]
388    #[serde(rename = "updatedMCPToolOutput")]
389    pub updated_mcp_tool_output: Option<serde_json::Value>,
390    #[serde(skip_serializing_if = "Option::is_none")]
391    #[serde(rename = "permissionRequestResult")]
392    pub permission_request_result: Option<PermissionRequestResult>,
393    #[serde(skip_serializing_if = "Option::is_none")]
394    pub retry: Option<bool>,
395}
396
397/// Hook outcome enum.
398#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
399#[serde(rename_all = "snake_case")]
400pub enum HookOutcome {
401    Success,
402    Blocking,
403    NonBlockingError,
404    Cancelled,
405}
406
407/// Permission behavior enum.
408#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
409#[serde(rename_all = "lowercase")]
410pub enum PermissionBehavior {
411    Ask,
412    Deny,
413    Allow,
414    Passthrough,
415}
416
417/// Aggregated result from running multiple hooks.
418#[derive(Debug, Clone, Serialize, Deserialize)]
419pub struct AggregatedHookResult {
420    #[serde(skip_serializing_if = "Option::is_none")]
421    pub message: Option<Message>,
422    #[serde(skip_serializing_if = "Option::is_none")]
423    #[serde(rename = "blockingErrors")]
424    pub blocking_errors: Option<Vec<HookBlockingError>>,
425    #[serde(skip_serializing_if = "Option::is_none")]
426    #[serde(rename = "preventContinuation")]
427    pub prevent_continuation: Option<bool>,
428    #[serde(skip_serializing_if = "Option::is_none")]
429    #[serde(rename = "stopReason")]
430    pub stop_reason: Option<String>,
431    #[serde(skip_serializing_if = "Option::is_none")]
432    #[serde(rename = "hookPermissionDecisionReason")]
433    pub hook_permission_decision_reason: Option<String>,
434    #[serde(skip_serializing_if = "Option::is_none")]
435    #[serde(rename = "permissionBehavior")]
436    pub permission_behavior: Option<String>,
437    #[serde(skip_serializing_if = "Option::is_none")]
438    #[serde(rename = "additionalContexts")]
439    pub additional_contexts: Option<Vec<String>>,
440    #[serde(skip_serializing_if = "Option::is_none")]
441    #[serde(rename = "initialUserMessage")]
442    pub initial_user_message: Option<String>,
443    #[serde(skip_serializing_if = "Option::is_none")]
444    #[serde(rename = "updatedInput")]
445    pub updated_input: Option<HashMap<String, serde_json::Value>>,
446    #[serde(skip_serializing_if = "Option::is_none")]
447    #[serde(rename = "updatedMCPToolOutput")]
448    pub updated_mcp_tool_output: Option<serde_json::Value>,
449    #[serde(skip_serializing_if = "Option::is_none")]
450    #[serde(rename = "permissionRequestResult")]
451    pub permission_request_result: Option<PermissionRequestResult>,
452    #[serde(skip_serializing_if = "Option::is_none")]
453    pub retry: Option<bool>,
454}