1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::process::Command;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum HookEvent {
7 PreToolUse,
8 PostToolUse,
9}
10
11impl HookEvent {
12 fn as_str(self) -> &'static str {
13 match self {
14 Self::PreToolUse => "PreToolUse",
15 Self::PostToolUse => "PostToolUse",
16 }
17 }
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct HookRunResult {
22 pub denied: bool,
23 pub messages: Vec<String>,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize, Default)]
27pub struct RuntimeHookConfig {
28 #[serde(default)]
29 pub pre_tool_use: Vec<String>,
30 #[serde(default)]
31 pub post_tool_use: Vec<String>,
32}
33
34pub struct HookRunner {
35 config: RuntimeHookConfig,
36}
37
38impl HookRunner {
39 pub fn new(config: RuntimeHookConfig) -> Self {
40 Self { config }
41 }
42
43 pub fn run_pre_tool_use(&self, tool_name: &str, tool_input: &Value) -> HookRunResult {
44 self.run_commands(
45 HookEvent::PreToolUse,
46 &self.config.pre_tool_use,
47 tool_name,
48 tool_input,
49 None,
50 false,
51 )
52 }
53
54 pub fn run_post_tool_use(
55 &self,
56 tool_name: &str,
57 tool_input: &Value,
58 tool_output: &str,
59 is_error: bool,
60 ) -> HookRunResult {
61 self.run_commands(
62 HookEvent::PostToolUse,
63 &self.config.post_tool_use,
64 tool_name,
65 tool_input,
66 Some(tool_output),
67 is_error,
68 )
69 }
70
71 fn run_commands(
72 &self,
73 event: HookEvent,
74 commands: &[String],
75 tool_name: &str,
76 tool_input: &Value,
77 tool_output: Option<&str>,
78 is_error: bool,
79 ) -> HookRunResult {
80 let mut messages = Vec::with_capacity(commands.len());
81 let mut denied = false;
82
83 for command_str in commands {
84 let mut cmd = if cfg!(windows) {
85 let mut c = Command::new("cmd");
86 c.arg("/C").arg(command_str);
87 c
88 } else {
89 let mut c = Command::new("sh");
90 c.arg("-c").arg(command_str);
91 c
92 };
93
94 cmd.env("HEMATITE_HOOK_EVENT", event.as_str());
95 cmd.env("HEMATITE_TOOL_NAME", tool_name);
96 cmd.env("HEMATITE_TOOL_INPUT", tool_input.to_string());
97 cmd.env("HEMATITE_TOOL_ERROR", if is_error { "1" } else { "0" });
98 if let Some(out) = tool_output {
99 cmd.env("HEMATITE_TOOL_OUTPUT", out);
100 }
101
102 match cmd.output() {
103 Ok(output) => {
104 let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
105 if !stdout.is_empty() {
106 messages.push(stdout);
107 }
108
109 if output.status.code() == Some(2) {
111 denied = true;
112 break;
113 }
114 }
115 Err(e) => {
116 messages.push(format!("Hook failed to start: {}", e));
117 }
118 }
119 }
120
121 HookRunResult { denied, messages }
122 }
123}