BigPizzaV3/CodexPlusPlus · critical · anyhow::Error

拒绝删除文件系统根目录:

Error message

拒绝删除文件系统根目录:{}

What it means

Thrown by `ensure_safe_recursive_removal` when the requested deletion target is empty or normalizes to a filesystem root (`/` on Unix; `C:\`, `\\?\C:\`, or UNC `\\server\share\` variants on Windows). This guard prevents a recursive delete from wiping an entire drive or share, including Windows cases where a plain `/` normalizes to the current drive root.

Solutions

  1. Fix the caller so it never passes an empty or root path — ensure a real subdirectory is computed before deletion
  2. Validate the target path before calling (non-empty, has a parent, not `has_root()` alone)
  3. Pass an explicit CODEX_HOME so normalization has a correct baseline and the other guards work
  4. Log/inspect `target.display()` (included in the error) to find where the root path originated

Example fix

// before
let target = std::env::var("CODEX_TARGET_DIR").unwrap_or_default(); // empty
ensure_safe_recursive_removal(Path::new(&target), &codex_home)?;
// after
let target = std::env::var("CODEX_TARGET_DIR").context("CODEX_TARGET_DIR must be set")?;
let path = Path::new(&target);
ensure!(path.is_absolute() && path.parent().is_some(), "refusing non-subdirectory target");
ensure_safe_recursive_removal(path, &codex_home)?;
Defensive patterns

Strategy: validation

Validate before calling

fn safe_removal_target(p: &Path) -> Result<(), String> {
    if p.as_os_str().is_empty() { return Err("empty path".into()); }
    if p.has_root() && p.parent().is_none() { return Err("filesystem root".into()); }
    Ok(())
}

Prevention

When it happens

Trigger: Calling the removal API with an empty path, `Path::new("/")`, a drive root like `C:\` or `C:`, a `\\?\C:\` verbatim path, or a UNC share root `\\server\share\`.

Common situations: Uninitialized/default empty path variables reaching the delete call, path-joining bugs that collapse to `/`, or user input containing a drive root on Windows.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19). Data as JSON: /api/errors/486c45bb2beb6f79. Report an issue: GitHub.

Appendix: source

Thrown at crates/codex-plus-core/src/codex_home.rs:36

/// `%USERPROFILE%\.codex`(454 MB 会话历史)被永久删除,只剩 1.1% 可恢复。我审计了
/// 全部 `remove_dir_all` 调用点,没有找到一条能删到 `.codex` 的既定路径——也就是说
/// 现有代码在正常输入下是安全的。但这也意味着:**一旦某个上游值(环境变量、被解析
/// 坏的路径、更新后失效的目录)指向了 home 本身,就没有任何东西拦得住它。**
/// 数据丢失不可逆,所以这里加一道与具体触发源无关的兜底。
///
/// 判定在**规范化之后**做,`..`、符号链接、大小写差异都拦得住;同时容忍路径尚不存在
/// (清理临时目录的常见情形,此时用词法规范化比较)。
pub fn ensure_safe_recursive_removal(target: &Path, codex_home: &Path) -> anyhow::Result<()> {
    let target = normalize_for_comparison(target);

    // 根路径 = 有根前缀且没有父目录,覆盖 POSIX 根(`/`)与 Windows 的各种写法
    // (`C:\`、`\\?\C:\`、UNC `\\server\share\`)。
    //
    // 不能只与 `Path::new("/")` 比较:Windows 上 `/` 不是绝对路径,会被 normalize
    // 成当前盘符根(如 `C:\`),相等比较拦不住它——也就是说递归删除盘符根本可以
    // 绕过这道守卫。`has_root()` 这一半也不可省:没有它 `C:` 会被误判成根。
    if target.as_os_str().is_empty() || is_filesystem_root(&target) {
        anyhow::bail!("拒绝删除文件系统根目录:{}", target.display());
    }
    let home = normalize_for_comparison(codex_home);
    if target == home {
        anyhow::bail!(
            "拒绝递归删除 CODEX_HOME 本身({})——这会连同全部会话历史一起丢失",
            target.display()
        );
    }
    if home.starts_with(&target) {
        anyhow::bail!(
            "拒绝删除 CODEX_HOME 的祖先目录 {}(CODEX_HOME = {})",
            target.display(),
            home.display()
        );
    }
    Ok(())
}

View on GitHub (pinned to b1ed92e5e4)