stynx_code_engine/application/
hook_runner.rs1use stynx_code_config::HooksConfig;
2use stynx_code_config::HookEntry;
3use tokio::process::Command;
4
5struct Outcome {
6 exit_code: i32,
7 stdout: String,
8}
9
10async fn run_cmd(command: &str, env_vars: &[(&str, &str)]) -> Outcome {
11 let (shell, flag) = stynx_code_config::shell_command();
12 let mut cmd = Command::new(shell);
13 cmd.arg(flag).arg(command);
14 for (k, v) in env_vars {
15 cmd.env(k, v);
16 }
17 match cmd.output().await {
18 Ok(out) => Outcome {
19 exit_code: out.status.code().unwrap_or(-1),
20 stdout: String::from_utf8_lossy(&out.stdout).trim().to_string(),
21 },
22 Err(_) => Outcome { exit_code: 1, stdout: String::new() },
23 }
24}
25
26fn tool_matches(entry: &HookEntry, tool_name: &str) -> bool {
27 match &entry.matcher {
28 None => true,
29 Some(m) if m.is_empty() => true,
30 Some(m) => tool_name.contains(m.as_str()),
31 }
32}
33
34pub struct PreToolDecision {
35 pub blocked: bool,
36 pub reason: String,
37 pub output: String,
38}
39
40pub async fn run_pre_tool_use(hooks: &HooksConfig, tool_name: &str, input_json: &str) -> PreToolDecision {
41 let env = [("CLAUDE_TOOL_NAME", tool_name), ("CLAUDE_TOOL_INPUT", input_json)];
42 for entry in hooks.pre_tool_use.iter().filter(|e| tool_matches(e, tool_name)) {
43 let out = run_cmd(&entry.command, &env).await;
44 if out.exit_code != 0 {
45 let reason = if out.stdout.is_empty() {
46 format!("blocked by hook (exit {})", out.exit_code)
47 } else {
48 out.stdout
49 };
50 return PreToolDecision { blocked: true, reason, output: String::new() };
51 }
52 if !out.stdout.is_empty() {
53 return PreToolDecision { blocked: false, reason: String::new(), output: out.stdout };
54 }
55 }
56 PreToolDecision { blocked: false, reason: String::new(), output: String::new() }
57}
58
59pub async fn run_post_tool_use(
60 hooks: &HooksConfig,
61 tool_name: &str,
62 input_json: &str,
63 tool_output: &str,
64) -> String {
65 let env = [
66 ("CLAUDE_TOOL_NAME", tool_name),
67 ("CLAUDE_TOOL_INPUT", input_json),
68 ("CLAUDE_TOOL_OUTPUT", tool_output),
69 ];
70 let mut out = String::new();
71 for entry in hooks.post_tool_use.iter().filter(|e| tool_matches(e, tool_name)) {
72 let result = run_cmd(&entry.command, &env).await;
73 if !result.stdout.is_empty() {
74 if !out.is_empty() { out.push('\n'); }
75 out.push_str(&result.stdout);
76 }
77 }
78 out
79}
80
81pub async fn run_stop_hooks(hooks: &HooksConfig) -> String {
82 let env: [(&str, &str); 0] = [];
83 let mut out = String::new();
84 for entry in &hooks.stop {
85 let result = run_cmd(&entry.command, &env).await;
86 if !result.stdout.is_empty() {
87 if !out.is_empty() { out.push('\n'); }
88 out.push_str(&result.stdout);
89 }
90 }
91 out
92}
93
94pub async fn run_session_start_hooks(hooks: &HooksConfig) -> String {
95 let env: [(&str, &str); 0] = [];
96 let mut out = String::new();
97 for entry in &hooks.session_start {
98 let result = run_cmd(&entry.command, &env).await;
99 if !result.stdout.is_empty() {
100 if !out.is_empty() { out.push('\n'); }
101 out.push_str(&result.stdout);
102 }
103 }
104 out
105}