openlatch_client/hooks/
claude_code.rs1use std::path::{Path, PathBuf};
8
9use serde_json::{json, Value};
10
11pub fn detect() -> Option<PathBuf> {
16 let home = dirs::home_dir()?;
18 let claude_dir = home.join(".claude");
19 if claude_dir.is_dir() {
20 Some(claude_dir)
21 } else {
22 None
23 }
24}
25
26pub fn settings_json_path(claude_dir: &Path) -> PathBuf {
28 claude_dir.join("settings.json")
29}
30
31pub fn build_hook_entry(event_type: &str, port: u16, token_env_var: &str) -> Value {
55 let url_path = match event_type {
56 "PreToolUse" => "pre-tool-use",
57 "UserPromptSubmit" => "user-prompt-submit",
58 "Stop" => "stop",
59 other => other,
61 };
62
63 let hook_inner = json!({
64 "type": "http",
65 "url": format!("http://localhost:{port}/hooks/{url_path}"),
66 "timeout": 10,
67 "headers": {
68 "Authorization": format!("Bearer ${{{token_env_var}}}")
69 },
70 "allowedEnvVars": [token_env_var]
71 });
72
73 if event_type == "PreToolUse" {
76 json!({
77 "matcher": "",
78 "_openlatch": true,
79 "hooks": [hook_inner]
80 })
81 } else {
82 json!({
83 "_openlatch": true,
84 "hooks": [hook_inner]
85 })
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 #[test]
94 fn test_build_hook_entry_pre_tool_use_has_matcher() {
95 let entry = build_hook_entry("PreToolUse", 7443, "OPENLATCH_TOKEN");
96 assert_eq!(entry["matcher"], "");
97 assert_eq!(entry["_openlatch"], true);
98 let url = entry["hooks"][0]["url"].as_str().unwrap();
99 assert_eq!(url, "http://localhost:7443/hooks/pre-tool-use");
100 }
101
102 #[test]
103 fn test_build_hook_entry_user_prompt_submit_no_matcher() {
104 let entry = build_hook_entry("UserPromptSubmit", 7443, "OPENLATCH_TOKEN");
105 assert!(
106 entry.get("matcher").is_none(),
107 "UserPromptSubmit must not have matcher field"
108 );
109 assert_eq!(entry["_openlatch"], true);
110 let url = entry["hooks"][0]["url"].as_str().unwrap();
111 assert_eq!(url, "http://localhost:7443/hooks/user-prompt-submit");
112 }
113
114 #[test]
115 fn test_build_hook_entry_stop_no_matcher() {
116 let entry = build_hook_entry("Stop", 7443, "OPENLATCH_TOKEN");
117 assert!(
118 entry.get("matcher").is_none(),
119 "Stop must not have matcher field"
120 );
121 assert_eq!(entry["_openlatch"], true);
122 let url = entry["hooks"][0]["url"].as_str().unwrap();
123 assert_eq!(url, "http://localhost:7443/hooks/stop");
124 }
125
126 #[test]
127 fn test_build_hook_entry_uses_env_var_name_not_value() {
128 let entry = build_hook_entry("PreToolUse", 7443, "OPENLATCH_TOKEN");
129 let auth = entry["hooks"][0]["headers"]["Authorization"]
131 .as_str()
132 .unwrap();
133 assert!(
134 auth.contains("${OPENLATCH_TOKEN}"),
135 "Auth header must use env var reference, got: {auth}"
136 );
137 let allowed = &entry["hooks"][0]["allowedEnvVars"];
139 assert_eq!(allowed[0], "OPENLATCH_TOKEN");
140 }
141
142 #[test]
143 fn test_build_hook_entry_openlatch_marker_present() {
144 for event_type in &["PreToolUse", "UserPromptSubmit", "Stop"] {
145 let entry = build_hook_entry(event_type, 7443, "OPENLATCH_TOKEN");
146 assert_eq!(
147 entry["_openlatch"], true,
148 "{event_type} entry must carry _openlatch:true marker"
149 );
150 }
151 }
152}