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