Skip to main content

agent_code_lib/hooks/
mod.rs

1//! Hook system.
2//!
3//! Hooks allow user-defined actions to run at specific points in the
4//! agent lifecycle:
5//!
6//! - `PreToolUse` — before a tool executes (can block/modify)
7//! - `PostToolUse` — after a tool completes
8//! - `SessionStart` — when a session begins
9//! - `SessionStop` — when a session ends
10//! - `UserPromptSubmit` — when the user submits input
11//!
12//! Hooks can be shell commands, HTTP endpoints, or prompt templates,
13//! configured in the settings file.
14
15// Hook types are defined in config::schema to avoid circular dependencies.
16// Re-export them here for convenience.
17pub use crate::config::{HookAction, HookDefinition, HookEvent};
18
19/// Hook registry that stores and dispatches hooks.
20pub struct HookRegistry {
21    hooks: Vec<HookDefinition>,
22}
23
24impl HookRegistry {
25    pub fn new() -> Self {
26        Self { hooks: Vec::new() }
27    }
28
29    pub fn register(&mut self, hook: HookDefinition) {
30        self.hooks.push(hook);
31    }
32
33    /// Get all hooks for a given event, optionally filtered by tool name.
34    pub fn get_hooks(&self, event: &HookEvent, tool_name: Option<&str>) -> Vec<&HookDefinition> {
35        self.hooks
36            .iter()
37            .filter(|h| {
38                h.event == *event
39                    && (h.tool_name.is_none()
40                        || tool_name.is_none()
41                        || h.tool_name.as_deref() == tool_name)
42            })
43            .collect()
44    }
45
46    /// Execute all hooks for a given event. Shell hooks run as subprocesses.
47    pub async fn run_hooks(
48        &self,
49        event: &HookEvent,
50        tool_name: Option<&str>,
51        _context: &serde_json::Value,
52    ) -> Vec<HookResult> {
53        let hooks = self.get_hooks(event, tool_name);
54        let mut results = Vec::new();
55
56        for hook in hooks {
57            let result = match &hook.action {
58                HookAction::Shell { command } => {
59                    match tokio::process::Command::new("bash")
60                        .arg("-c")
61                        .arg(command)
62                        .output()
63                        .await
64                    {
65                        Ok(output) => HookResult {
66                            success: output.status.success(),
67                            output: String::from_utf8_lossy(&output.stdout).to_string(),
68                        },
69                        Err(e) => HookResult {
70                            success: false,
71                            output: e.to_string(),
72                        },
73                    }
74                }
75                HookAction::Http { url, method } => {
76                    let client = reqwest::Client::new();
77                    let method = method.as_deref().unwrap_or("POST");
78                    let req = match method {
79                        "GET" => client.get(url),
80                        _ => client.post(url),
81                    };
82                    match req.send().await {
83                        Ok(resp) => HookResult {
84                            success: resp.status().is_success(),
85                            output: resp.text().await.unwrap_or_default(),
86                        },
87                        Err(e) => HookResult {
88                            success: false,
89                            output: e.to_string(),
90                        },
91                    }
92                }
93            };
94            results.push(result);
95        }
96
97        results
98    }
99}
100
101/// Result of executing a hook.
102#[derive(Debug)]
103pub struct HookResult {
104    pub success: bool,
105    pub output: String,
106}