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    if level == "hard" {
23        println!("  Hard mode = Replace mode: denying native Read/Grep/Glob/Bash across all IDEs.");
24        println!();
25        // Trigger a full Replace-mode setup for all detected agents
26        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}