pub(super) fn effective_allowlist() -> Vec<String> {
if let Ok(ov) = std::env::var("LEAN_CTX_SHELL_ALLOWLIST_OVERRIDE") {
return ov
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
}
let cfg = crate::core::config::Config::load();
let mut list = cfg.shell_allowlist;
if !list.is_empty() {
for entry in cfg.shell_allowlist_extra {
if !entry.is_empty() && !list.contains(&entry) {
list.push(entry);
}
}
}
if let Ok(env_val) = std::env::var("LEAN_CTX_SHELL_ALLOWLIST") {
for entry in env_val
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
{
if !list.contains(&entry) {
list.push(entry);
}
}
}
list
}
pub(super) fn allowlist_block_message(base: &str) -> String {
let cfg_path = crate::core::config::Config::path().map_or_else(
|| "~/.lean-ctx/config.toml".to_string(),
|p| p.display().to_string(),
);
let mut msg = format!(
"[BLOCKED — DO NOT RETRY] '{base}' is not in the shell allowlist. \
This is a permanent restriction, not a transient error.\n\
Fix (additive, keeps the defaults): run lean-ctx allow {base}\n\
Config in effect: {cfg_path}\n\
Or disable the allowlist entirely: set shell_allowlist = []\n\
Or turn off all shell gating (you own the risk): set shell_security = \"off\" \
(or env LEAN_CTX_SHELL_SECURITY=off) — compression still applies.\n\
Do NOT reroute through ctx_execute(language=\"shell\"): both tools enforce the same \
policy. Allow the command explicitly or change shell_security deliberately."
);
if crate::core::config::cloud_infra_commands().contains(&base) {
msg.push_str(
"\nNote: cloud/infra CLIs (terraform, kubectl, aws, …) are deliberately \
excluded from the defaults — they mutate remote infrastructure with \
ambient credentials. Opting in is a deliberate user decision.",
);
}
if let Some(parse_err) = crate::core::config::last_config_parse_error() {
msg.push_str(&format!(
"\n\n⚠ Your config.toml currently FAILS to parse, so lean-ctx is running on the \
built-in defaults — this is almost certainly why editing the allowlist had no \
effect. Fix the TOML error below, then retry:\n {parse_err}\n File: {cfg_path}"
));
} else if let Some(missing) = crate::core::config::Config::missing_config_path() {
msg.push_str(&format!(
"\n\n⚠ No config file exists at {} — lean-ctx is running on built-in defaults. \
If you added the command to a config.toml in a DIFFERENT location (XDG \
~/.config/lean-ctx vs legacy ~/.lean-ctx, or your MCP client launches lean-ctx \
in a sandbox/container with a different HOME), the runtime never reads it. \
`lean-ctx doctor` prints the path actually in effect; pin it with \
LEAN_CTX_CONFIG_DIR.",
missing.display()
));
}
if let Some(notice) = crate::core::workspace_trust::untrusted_override_notice() {
msg.push_str("\n\n⚠ ");
msg.push_str(¬ice);
}
msg
}
#[must_use]
pub fn effective_allowlist_pub() -> Vec<String> {
effective_allowlist()
}