1use std::path::PathBuf;
2
3pub fn run(args: &[String]) {
4 let undo = args.iter().any(|a| a == "--undo");
5 let level = if args.iter().any(|a| a == "--hard") {
6 "hard"
7 } else {
8 "soft"
9 };
10
11 if undo {
12 undo_harden();
13 } else {
14 apply_harden(level);
15 }
16}
17
18fn apply_harden(level: &str) {
19 println!("lean-ctx harden (level: {level})");
20 println!();
21
22 if level == "hard" {
23 println!(" Hard mode = Replace mode: denying native Read/Grep/Glob across all IDEs.");
24 println!();
25 let opts = crate::setup::SetupOptions {
26 non_interactive: true,
27 yes: true,
28 fix: true,
29 ..Default::default()
30 };
31 if let Err(e) = crate::setup::run_setup_with_options(opts) {
32 eprintln!(" Setup error: {e}");
33 }
34 println!();
35 println!("Replace mode active. All native tools denied — use ctx_* MCP tools.");
36 println!("Undo with: lean-ctx harden --undo");
37 return;
38 }
39
40 let mut applied = Vec::new();
41
42 if set_env_in_mcp_configs() {
43 applied.push("Set LEAN_CTX_HARDEN=1 in MCP configs");
44 }
45
46 if cleanup_claude_stale_bash_deny() {
47 applied.push("Claude Code: removed stale Bash from permissions.deny (GH #799)");
48 }
49
50 if applied.is_empty() {
51 println!(" Nothing to harden (no supported editors detected).");
52 } else {
53 println!();
54 for item in &applied {
55 println!(" [OK] {item}");
56 }
57 println!();
58 println!("Harden active. Native Read/Grep will be denied (except after Edit).");
59 println!("Undo with: lean-ctx harden --undo");
60 }
61}
62
63fn undo_harden() {
64 println!("lean-ctx harden --undo");
65 println!();
66
67 remove_env_from_mcp_configs();
68 remove_claude_permissions_deny();
69
70 println!(" [OK] Harden deactivated. Native tools allowed again.");
71}
72
73fn set_env_in_mcp_configs() -> bool {
74 let targets = discover_mcp_configs();
75 let mut any_set = false;
76
77 for path in targets {
78 if let Ok(content) = std::fs::read_to_string(&path)
79 && let Ok(mut json) = crate::core::jsonc::parse_jsonc(&content)
80 && let Some(servers) = find_lean_ctx_server_mut(&mut json)
81 {
82 let env = servers
83 .as_object_mut()
84 .and_then(|s| s.get_mut("env"))
85 .and_then(|e| e.as_object_mut());
86
87 if let Some(env_map) = env {
88 env_map.insert(
89 "LEAN_CTX_HARDEN".to_string(),
90 serde_json::Value::String("1".to_string()),
91 );
92 } else if let Some(server_obj) = servers.as_object_mut() {
93 let mut env_map = serde_json::Map::new();
94 env_map.insert(
95 "LEAN_CTX_HARDEN".to_string(),
96 serde_json::Value::String("1".to_string()),
97 );
98 server_obj.insert("env".to_string(), serde_json::Value::Object(env_map));
99 }
100
101 if let Ok(out) = serde_json::to_string_pretty(&json) {
102 let _ = std::fs::write(&path, out);
103 any_set = true;
104 println!(" [OK] {}", path.display());
105 }
106 }
107 }
108 any_set
109}
110
111fn remove_env_from_mcp_configs() {
112 for path in discover_mcp_configs() {
113 if let Ok(content) = std::fs::read_to_string(&path)
114 && let Ok(mut json) = crate::core::jsonc::parse_jsonc(&content)
115 && let Some(servers) = find_lean_ctx_server_mut(&mut json)
116 && let Some(env) = servers
117 .as_object_mut()
118 .and_then(|s| s.get_mut("env"))
119 .and_then(|e| e.as_object_mut())
120 {
121 env.remove("LEAN_CTX_HARDEN");
122 if let Ok(out) = serde_json::to_string_pretty(&json) {
123 let _ = std::fs::write(&path, out);
124 }
125 }
126 }
127}
128
129fn cleanup_claude_stale_bash_deny() -> bool {
136 let Some(home) = dirs::home_dir() else {
137 return false;
138 };
139 let settings_path = home.join(".claude").join("settings.json");
140 if !settings_path.exists() {
141 return false;
142 }
143
144 let Ok(content) = std::fs::read_to_string(&settings_path) else {
145 return false;
146 };
147 let Ok(mut json) = crate::core::jsonc::parse_jsonc(&content) else {
148 return false;
149 };
150
151 let removed = if let Some(deny) = json
152 .pointer_mut("/permissions/deny")
153 .and_then(|d| d.as_array_mut())
154 {
155 let before = deny.len();
156 deny.retain(|v| v.as_str() != Some("Bash"));
157 deny.len() < before
158 } else {
159 false
160 };
161
162 if removed && let Ok(out) = serde_json::to_string_pretty(&json) {
163 let _ = std::fs::write(&settings_path, out);
164 }
165 removed
166}
167
168fn remove_claude_permissions_deny() {
169 let Some(home) = dirs::home_dir() else {
170 return;
171 };
172 let settings_path = home.join(".claude").join("settings.json");
173 if !settings_path.exists() {
174 return;
175 }
176
177 let Ok(content) = std::fs::read_to_string(&settings_path) else {
178 return;
179 };
180 let Ok(mut json) = crate::core::jsonc::parse_jsonc(&content) else {
181 return;
182 };
183
184 if let Some(deny) = json
185 .pointer_mut("/permissions/deny")
186 .and_then(|d| d.as_array_mut())
187 {
188 deny.retain(|v| v.as_str() != Some("Bash"));
189 }
190
191 if let Ok(out) = serde_json::to_string_pretty(&json) {
192 let _ = std::fs::write(&settings_path, out);
193 }
194}
195
196fn discover_mcp_configs() -> Vec<PathBuf> {
197 let Some(home) = dirs::home_dir() else {
198 return Vec::new();
199 };
200
201 let candidates = [
202 home.join(".cursor").join("mcp.json"),
203 home.join(".claude.json"),
204 home.join(".codebuddy.json"),
205 home.join(".codeium")
206 .join("windsurf")
207 .join("mcp_config.json"),
208 ];
209
210 candidates.into_iter().filter(|p| p.exists()).collect()
211}
212
213fn find_lean_ctx_server_mut(json: &mut serde_json::Value) -> Option<&mut serde_json::Value> {
214 if let Some(servers) = json.get_mut("mcpServers")
215 && let Some(lctx) = servers.get_mut("lean-ctx")
216 {
217 return Some(lctx);
218 }
219 None
220}