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}