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