git_worktree_manager/operations/
claude_settings.rs1use serde_json::json;
10
11use crate::error::Result;
12
13const GW_BIN: &str = "gw";
14
15pub fn guard_settings_json() -> Result<String> {
18 let payload = json!({
19 "hooks": {
20 "PreToolUse": [
21 pre_tool_use_bash_entry()?
22 ]
23 }
24 });
25
26 Ok(payload.to_string())
27}
28
29pub fn pre_tool_use_bash_entry() -> Result<serde_json::Value> {
32 Ok(json!({
33 "matcher": "Bash",
34 "hooks": [
35 { "type": "command", "command": format!("{GW_BIN} guard --tool-input -") }
36 ]
37 }))
38}
39
40pub fn worktree_create_entry() -> Result<serde_json::Value> {
43 Ok(json!({
44 "hooks": [
45 { "type": "command", "command": format!("{GW_BIN} _claude-worktree-create") }
46 ]
47 }))
48}
49
50pub fn worktree_remove_entry() -> Result<serde_json::Value> {
53 Ok(json!({
54 "hooks": [
55 { "type": "command", "command": format!("{GW_BIN} _claude-worktree-remove") }
56 ]
57 }))
58}
59
60pub(crate) fn is_legacy_gw_command(command: &str, suffix: &str) -> bool {
65 if !command.ends_with(suffix) {
66 return false;
67 }
68 let prefix = command.trim_end_matches(suffix).trim_end();
71 !prefix.is_empty() && prefix != GW_BIN
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77 use serde_json::Value;
78
79 fn parse(json: &str) -> Value {
80 serde_json::from_str(json).expect("valid json")
81 }
82
83 #[test]
84 fn payload_has_pretooluse_bash_hook() {
85 let s = guard_settings_json().expect("ok");
86 let v = parse(&s);
87 let arr = v["hooks"]["PreToolUse"]
88 .as_array()
89 .expect("PreToolUse array");
90 assert_eq!(arr.len(), 1);
91 assert_eq!(arr[0]["matcher"], "Bash");
92 let inner = arr[0]["hooks"].as_array().expect("inner hooks");
93 assert_eq!(inner.len(), 1);
94 assert_eq!(inner[0]["type"], "command");
95 let cmd = inner[0]["command"].as_str().expect("command str");
96 assert_eq!(cmd, "gw guard --tool-input -");
97 }
98
99 #[test]
100 fn pre_tool_use_bash_entry_is_bare_gw() {
101 let entry = pre_tool_use_bash_entry().expect("ok");
102 assert_eq!(entry["matcher"], "Bash");
103 let inner = entry["hooks"].as_array().expect("inner hooks array");
104 assert_eq!(inner[0]["type"], "command");
105 assert_eq!(inner[0]["command"], "gw guard --tool-input -");
106 }
107
108 #[test]
109 fn worktree_create_entry_is_bare_gw() {
110 let entry = worktree_create_entry().expect("ok");
111 assert!(entry.get("matcher").is_none());
113 let inner = entry["hooks"].as_array().expect("inner hooks array");
114 assert_eq!(inner[0]["type"], "command");
115 assert_eq!(inner[0]["command"], "gw _claude-worktree-create");
116 }
117
118 #[test]
119 fn worktree_remove_entry_is_bare_gw() {
120 let entry = worktree_remove_entry().expect("ok");
121 assert!(entry.get("matcher").is_none());
123 let inner = entry["hooks"].as_array().expect("inner hooks array");
124 assert_eq!(inner[0]["type"], "command");
125 assert_eq!(inner[0]["command"], "gw _claude-worktree-remove");
126 }
127
128 #[test]
129 fn is_legacy_recognizes_absolute_paths() {
130 assert!(is_legacy_gw_command(
131 "/usr/local/bin/gw guard --tool-input -",
132 " guard --tool-input -",
133 ));
134 assert!(is_legacy_gw_command(
135 "/opt/homebrew/bin/gw _claude-worktree-create",
136 " _claude-worktree-create",
137 ));
138 assert!(is_legacy_gw_command(
139 "/Users/dave/proj/target/release/gw _claude-worktree-remove",
140 " _claude-worktree-remove",
141 ));
142 }
143
144 #[test]
145 fn is_legacy_rejects_current_bare_form() {
146 assert!(!is_legacy_gw_command(
148 "gw guard --tool-input -",
149 " guard --tool-input -",
150 ));
151 assert!(!is_legacy_gw_command(
152 "gw _claude-worktree-create",
153 " _claude-worktree-create",
154 ));
155 }
156
157 #[test]
158 fn is_legacy_rejects_unrelated_commands() {
159 assert!(!is_legacy_gw_command(
160 "/usr/local/bin/some-other-tool guard --tool-input -",
161 " _claude-worktree-create", ));
163 assert!(!is_legacy_gw_command("", " guard --tool-input -"));
164 }
165}