pub(crate) fn check_write_safety(path: &str, full: &std::path::Path, content: &str) -> anyhow::Result<()> {
if path.starts_with('/') || path.starts_with('\\') || path.contains(":\\") {
anyhow::bail!(
"[blocked] absolute path not allowed: {path}. Use relative paths within workspace."
);
}
if path.contains("../") || path.contains("..\\") {
anyhow::bail!("[blocked] path traversal not allowed: {path}");
}
let path_lower = path.to_lowercase();
const SENSITIVE_NAMES: &[&str] = &[
".bashrc",
".bash_profile",
".zshrc",
".profile",
".login",
"authorized_keys",
"known_hosts",
"id_rsa",
"id_ed25519",
"crontab",
".env",
"openclaw.json",
"rsclaw.json5",
"auth-profiles.json",
];
let filename = full
.file_name()
.map(|f| f.to_string_lossy().to_lowercase())
.unwrap_or_default();
for sensitive in SENSITIVE_NAMES {
if filename == *sensitive || path_lower.ends_with(sensitive) {
anyhow::bail!("[blocked] write to sensitive file: {path}");
}
}
if !content.is_empty() {
let preparse = crate::agent::preparse::PreParseEngine::load();
for line in content.lines() {
let trimmed = line.trim();
if trimmed.is_empty()
|| trimmed.starts_with('#')
|| trimmed.starts_with("//")
|| trimmed.starts_with("--")
{
continue;
}
match preparse.check_exec_safety(trimmed) {
crate::agent::preparse::SafetyCheck::Deny(reason) => {
anyhow::bail!("[blocked] file contains dangerous command: {reason}");
}
_ => {}
}
}
}
Ok(())
}
pub(crate) fn check_read_safety(path: &str, full: &std::path::Path) -> anyhow::Result<()> {
let path_str = full.to_string_lossy().to_lowercase();
let path_lower = path.to_lowercase();
const SENSITIVE_DIRS: &[&str] = &[
".ssh/",
".gnupg/",
".gpg/",
".aws/",
".azure/",
".gcloud/",
".config/gcloud/",
".kube/",
".docker/",
".claude/",
".opencode/",
".openclaw/credentials/",
".rsclaw/credentials/",
];
for dir in SENSITIVE_DIRS {
if path_lower.contains(dir) || path_str.contains(dir) {
anyhow::bail!("[blocked] access to sensitive directory: {path}");
}
}
let filename = full
.file_name()
.map(|f| f.to_string_lossy().to_lowercase())
.unwrap_or_default();
const SENSITIVE_FILES: &[&str] = &[
"id_rsa",
"id_ed25519",
"id_ecdsa",
"id_dsa",
"id_rsa.pub",
"id_ed25519.pub",
"authorized_keys",
"known_hosts",
"secring.gpg",
"trustdb.gpg",
"credentials",
"credentials.json",
"credentials.yaml",
"service_account.json",
"application_default_credentials.json",
".env",
".env.local",
".env.production",
".env.secret",
".netrc",
".npmrc",
".pypirc",
".bash_history",
".zsh_history",
".pgpass",
".my.cnf",
".mongoshrc.js",
"config.json", "wallet.dat",
"keystore",
"openclaw.json",
"rsclaw.json5",
"auth-profiles.json",
];
for sensitive in SENSITIVE_FILES {
if filename == *sensitive {
anyhow::bail!("[blocked] access to sensitive file: {path}");
}
}
if filename.contains("private") && (filename.contains("key") || filename.ends_with(".pem")) {
anyhow::bail!("[blocked] access to private key file: {path}");
}
const SYSTEM_FILES: &[&str] = &[
"/etc/shadow",
"/etc/gshadow",
"/etc/master.passwd",
"/etc/sudoers",
];
for sys in SYSTEM_FILES {
if path_str.ends_with(sys) || path == *sys {
anyhow::bail!("[blocked] access to system file: {path}");
}
}
Ok(())
}
pub(crate) fn check_file_content_safety(file_path: &std::path::Path) -> anyhow::Result<()> {
let content = match std::fs::read_to_string(file_path) {
Ok(c) => c,
Err(_) => return Ok(()), };
let preparse = crate::agent::preparse::PreParseEngine::load();
for (line_num, line) in content.lines().enumerate() {
let trimmed = line.trim();
if trimmed.is_empty()
|| trimmed.starts_with('#')
|| trimmed.starts_with("//")
|| trimmed.starts_with("--")
{
continue;
}
match preparse.check_exec_safety(trimmed) {
crate::agent::preparse::SafetyCheck::Deny(reason) => {
anyhow::bail!(
"[blocked] file {}:{} contains dangerous command: {reason}",
file_path.display(),
line_num + 1
);
}
_ => {}
}
}
Ok(())
}