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        && let Some(msg) = apply_claude_permissions_deny()
30    {
31        applied.push("Claude Code: added Bash to permissions.deny");
32        println!("  {msg}");
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            && let Ok(mut json) = crate::core::jsonc::parse_jsonc(&content)
65            && let Some(servers) = find_lean_ctx_server_mut(&mut json)
66        {
67            let env = servers
68                .as_object_mut()
69                .and_then(|s| s.get_mut("env"))
70                .and_then(|e| e.as_object_mut());
71
72            if let Some(env_map) = env {
73                env_map.insert(
74                    "LEAN_CTX_HARDEN".to_string(),
75                    serde_json::Value::String("1".to_string()),
76                );
77            } else if let Some(server_obj) = servers.as_object_mut() {
78                let mut env_map = serde_json::Map::new();
79                env_map.insert(
80                    "LEAN_CTX_HARDEN".to_string(),
81                    serde_json::Value::String("1".to_string()),
82                );
83                server_obj.insert("env".to_string(), serde_json::Value::Object(env_map));
84            }
85
86            if let Ok(out) = serde_json::to_string_pretty(&json) {
87                let _ = std::fs::write(&path, out);
88                any_set = true;
89                println!("  [OK] {}", path.display());
90            }
91        }
92    }
93    any_set
94}
95
96fn remove_env_from_mcp_configs() {
97    for path in discover_mcp_configs() {
98        if let Ok(content) = std::fs::read_to_string(&path)
99            && let Ok(mut json) = crate::core::jsonc::parse_jsonc(&content)
100            && let Some(servers) = find_lean_ctx_server_mut(&mut json)
101            && let Some(env) = servers
102                .as_object_mut()
103                .and_then(|s| s.get_mut("env"))
104                .and_then(|e| e.as_object_mut())
105        {
106            env.remove("LEAN_CTX_HARDEN");
107            if let Ok(out) = serde_json::to_string_pretty(&json) {
108                let _ = std::fs::write(&path, out);
109            }
110        }
111    }
112}
113
114fn apply_claude_permissions_deny() -> Option<&'static str> {
115    let home = dirs::home_dir()?;
116    let settings_path = home.join(".claude").join("settings.json");
117
118    let mut json = if settings_path.exists() {
119        let content = std::fs::read_to_string(&settings_path).ok()?;
120        crate::core::jsonc::parse_jsonc(&content).ok()?
121    } else {
122        serde_json::json!({})
123    };
124
125    let obj = json.as_object_mut()?;
126
127    let permissions = obj
128        .entry("permissions")
129        .or_insert_with(|| serde_json::json!({}));
130    let deny = permissions
131        .as_object_mut()?
132        .entry("deny")
133        .or_insert_with(|| serde_json::json!([]));
134
135    if let Some(arr) = deny.as_array_mut() {
136        let bash_str = serde_json::Value::String("Bash".to_string());
137        if !arr.contains(&bash_str) {
138            arr.push(bash_str);
139        }
140    }
141
142    let out = serde_json::to_string_pretty(&json).ok()?;
143    std::fs::write(&settings_path, out).ok()?;
144    Some("Added 'Bash' to ~/.claude/settings.json permissions.deny")
145}
146
147fn remove_claude_permissions_deny() {
148    let Some(home) = dirs::home_dir() else {
149        return;
150    };
151    let settings_path = home.join(".claude").join("settings.json");
152    if !settings_path.exists() {
153        return;
154    }
155
156    let Ok(content) = std::fs::read_to_string(&settings_path) else {
157        return;
158    };
159    let Ok(mut json) = crate::core::jsonc::parse_jsonc(&content) else {
160        return;
161    };
162
163    if let Some(deny) = json
164        .pointer_mut("/permissions/deny")
165        .and_then(|d| d.as_array_mut())
166    {
167        deny.retain(|v| v.as_str() != Some("Bash"));
168    }
169
170    if let Ok(out) = serde_json::to_string_pretty(&json) {
171        let _ = std::fs::write(&settings_path, out);
172    }
173}
174
175fn discover_mcp_configs() -> Vec<PathBuf> {
176    let Some(home) = dirs::home_dir() else {
177        return Vec::new();
178    };
179
180    let candidates = [
181        home.join(".cursor").join("mcp.json"),
182        home.join(".claude.json"),
183        home.join(".codebuddy.json"),
184        home.join(".codeium")
185            .join("windsurf")
186            .join("mcp_config.json"),
187    ];
188
189    candidates.into_iter().filter(|p| p.exists()).collect()
190}
191
192fn find_lean_ctx_server_mut(json: &mut serde_json::Value) -> Option<&mut serde_json::Value> {
193    if let Some(servers) = json.get_mut("mcpServers")
194        && let Some(lctx) = servers.get_mut("lean-ctx")
195    {
196        return Some(lctx);
197    }
198    None
199}