use std::path::{Path, PathBuf};
use serde_json::{json, Value};
pub fn detect() -> Option<PathBuf> {
let home = dirs::home_dir()?;
let claude_dir = home.join(".claude");
if claude_dir.is_dir() {
Some(claude_dir)
} else {
None
}
}
pub fn settings_json_path(claude_dir: &Path) -> PathBuf {
claude_dir.join("settings.json")
}
pub fn build_hook_entry(event_type: &str, port: u16, token_env_var: &str) -> Value {
let url_path = match event_type {
"PreToolUse" => "pre-tool-use",
"UserPromptSubmit" => "user-prompt-submit",
"Stop" => "stop",
other => other,
};
let hook_inner = json!({
"type": "http",
"url": format!("http://localhost:{port}/hooks/{url_path}"),
"timeout": 10,
"headers": {
"Authorization": format!("Bearer ${{{token_env_var}}}")
},
"allowedEnvVars": [token_env_var]
});
if event_type == "PreToolUse" {
json!({
"matcher": "",
"_openlatch": true,
"hooks": [hook_inner]
})
} else {
json!({
"_openlatch": true,
"hooks": [hook_inner]
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_hook_entry_pre_tool_use_has_matcher() {
let entry = build_hook_entry("PreToolUse", 7443, "OPENLATCH_TOKEN");
assert_eq!(entry["matcher"], "");
assert_eq!(entry["_openlatch"], true);
let url = entry["hooks"][0]["url"].as_str().unwrap();
assert_eq!(url, "http://localhost:7443/hooks/pre-tool-use");
}
#[test]
fn test_build_hook_entry_user_prompt_submit_no_matcher() {
let entry = build_hook_entry("UserPromptSubmit", 7443, "OPENLATCH_TOKEN");
assert!(
entry.get("matcher").is_none(),
"UserPromptSubmit must not have matcher field"
);
assert_eq!(entry["_openlatch"], true);
let url = entry["hooks"][0]["url"].as_str().unwrap();
assert_eq!(url, "http://localhost:7443/hooks/user-prompt-submit");
}
#[test]
fn test_build_hook_entry_stop_no_matcher() {
let entry = build_hook_entry("Stop", 7443, "OPENLATCH_TOKEN");
assert!(
entry.get("matcher").is_none(),
"Stop must not have matcher field"
);
assert_eq!(entry["_openlatch"], true);
let url = entry["hooks"][0]["url"].as_str().unwrap();
assert_eq!(url, "http://localhost:7443/hooks/stop");
}
#[test]
fn test_build_hook_entry_uses_env_var_name_not_value() {
let entry = build_hook_entry("PreToolUse", 7443, "OPENLATCH_TOKEN");
let auth = entry["hooks"][0]["headers"]["Authorization"]
.as_str()
.unwrap();
assert!(
auth.contains("${OPENLATCH_TOKEN}"),
"Auth header must use env var reference, got: {auth}"
);
let allowed = &entry["hooks"][0]["allowedEnvVars"];
assert_eq!(allowed[0], "OPENLATCH_TOKEN");
}
#[test]
fn test_build_hook_entry_openlatch_marker_present() {
for event_type in &["PreToolUse", "UserPromptSubmit", "Stop"] {
let entry = build_hook_entry(event_type, 7443, "OPENLATCH_TOKEN");
assert_eq!(
entry["_openlatch"], true,
"{event_type} entry must carry _openlatch:true marker"
);
}
}
}