Skip to main content

lean_ctx/cli/
harden.rs

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}