git_worktree_manager/operations/
claude_settings.rs1use std::path::PathBuf;
10
11use serde_json::json;
12
13use crate::error::{CwError, Result};
14use crate::operations::spawn_spec::quote_path_for_shell;
15
16pub fn guard_settings_json() -> Result<String> {
24 let self_exe = resolve_self_exe()?;
25 let command = format!("{} guard --tool-input -", quote_path_for_shell(&self_exe));
26
27 let payload = json!({
28 "hooks": {
29 "PreToolUse": [
30 {
31 "matcher": "Bash",
32 "hooks": [
33 { "type": "command", "command": command }
34 ]
35 }
36 ]
37 }
38 });
39
40 Ok(payload.to_string())
41}
42
43fn resolve_self_exe() -> Result<PathBuf> {
44 if let Some(s) = std::env::var_os("CW_SPAWN_AI_BIN") {
45 return Ok(PathBuf::from(s));
46 }
47 std::env::current_exe().map_err(|e| {
48 CwError::Other(format!(
49 "claude_settings: cannot resolve current executable path: {}. \
50 Set CW_SPAWN_AI_BIN to override.",
51 e
52 ))
53 })
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59 use crate::operations::test_env::{env_lock, EnvGuard};
60 use serde_json::Value;
61
62 fn parse(json: &str) -> Value {
63 serde_json::from_str(json).expect("valid json")
64 }
65
66 #[test]
67 fn payload_has_pretooluse_bash_hook() {
68 let _lock = env_lock();
69 let _guard = EnvGuard::capture(&["CW_SPAWN_AI_BIN"]);
70 std::env::set_var("CW_SPAWN_AI_BIN", "/usr/local/bin/gw");
71
72 let s = guard_settings_json().expect("ok");
73 let v = parse(&s);
74 let arr = v["hooks"]["PreToolUse"]
75 .as_array()
76 .expect("PreToolUse array");
77 assert_eq!(arr.len(), 1);
78 assert_eq!(arr[0]["matcher"], "Bash");
79 let inner = arr[0]["hooks"].as_array().expect("inner hooks");
80 assert_eq!(inner.len(), 1);
81 assert_eq!(inner[0]["type"], "command");
82 let cmd = inner[0]["command"].as_str().expect("command str");
83 assert!(cmd.ends_with(" guard --tool-input -"));
84 assert!(cmd.contains("/usr/local/bin/gw"));
85 }
86
87 #[test]
88 fn cw_spawn_ai_bin_overrides_path() {
89 let _lock = env_lock();
90 let _guard = EnvGuard::capture(&["CW_SPAWN_AI_BIN"]);
91 std::env::set_var("CW_SPAWN_AI_BIN", "/tmp/custom/gw");
92
93 let s = guard_settings_json().expect("ok");
94 let v = parse(&s);
95 let cmd = v["hooks"]["PreToolUse"][0]["hooks"][0]["command"]
96 .as_str()
97 .expect("str");
98 assert!(cmd.contains("/tmp/custom/gw"));
99 }
100
101 #[test]
102 fn path_with_spaces_is_quoted() {
103 let _lock = env_lock();
104 let _guard = EnvGuard::capture(&["CW_SPAWN_AI_BIN"]);
105 std::env::set_var("CW_SPAWN_AI_BIN", "/tmp/dir with space/gw");
106
107 let s = guard_settings_json().expect("ok");
108 let v = parse(&s);
109 let cmd = v["hooks"]["PreToolUse"][0]["hooks"][0]["command"]
110 .as_str()
111 .expect("str");
112 assert!(cmd.starts_with("\"/tmp/dir with space/gw\""));
113 }
114
115 #[test]
116 fn windows_backslash_path_is_quoted() {
117 let _lock = env_lock();
118 let _guard = EnvGuard::capture(&["CW_SPAWN_AI_BIN"]);
119 std::env::set_var("CW_SPAWN_AI_BIN", r"C:\Program Files\gw\gw.exe");
120
121 let s = guard_settings_json().expect("ok");
122 let v = parse(&s);
123 let cmd = v["hooks"]["PreToolUse"][0]["hooks"][0]["command"]
124 .as_str()
125 .expect("str");
126 assert!(cmd.starts_with("\"C:\\Program Files\\gw\\gw.exe\""));
127 }
128}