BigPizzaV3/CodexPlusPlus · critical · anyhow::Error

拒绝删除 CODEX_HOME 的祖先目录

Error message

拒绝删除 CODEX_HOME 的祖先目录 {}(CODEX_HOME = {})

What it means

ensure_safe_recursive_removal is a data-loss guard invoked before any recursive delete. It throws this error when the deletion target is an ancestor directory of CODEX_HOME (e.g. the user's home directory or drive root parent), meaning deleting it would also destroy the entire CODEX_HOME tree including all session history. It is a source-agnostic last line of defense added after incident #2146 where 454 MB of history was permanently lost.

Solutions

  1. Inspect the target being deleted — it must be a leaf directory BELOW CODEX_HOME (or unrelated to it), never a parent
  2. Check the CODEX_HOME value (env var or profile setting) for mistakes like ~/ or a drive/mount root
  3. Verify how the target path was constructed; remove any `..` components or symlinked segments that resolve it above CODEX_HOME
  4. If you genuinely need to delete the whole CODEX_HOME, do not route it through this guarded API — the guard is intentionally irreversible

Example fix

// before
ensure_safe_recursive_removal(&home_dir.join(".codex/.."), &codex_home)?;
// after
let session_dir = codex_home.join("sessions").canonicalize()?;
ensure_safe_recursive_removal(&session_dir, &codex_home)?;
Defensive patterns

Strategy: validation

Validate before calling

fn is_safe_removal_target(target: &Path, codex_home: &Path) -> bool {
    let t = target.canonicalize().unwrap_or_else(|_| target.to_path_buf());
    let h = codex_home.canonicalize().unwrap_or_else(|_| codex_home.to_path_buf());
    t != h && !h.starts_with(&t) && t.as_os_str() != "/"
}
// call ensure_safe_recursive_removal only if this returns true

Prevention

When it happens

Trigger: Calling ensure_safe_recursive_removal(target, codex_home) where, after normalization, home.starts_with(target) is true — i.e. target is a parent (any level) of CODEX_HOME, such as passing ~/.config when CODEX_HOME=~/.config/codex, or an env var / corrupted path that resolved to an ancestor.

Common situations: CODEX_HOME env var changed or resolved unexpectedly (e.g. set to ~/ or an empty-ish value falling back to a default under the target), cleanup routines computing the wrong base directory, path aliasing via symlinks or `..` segments that normalize the target into an ancestor, macOS /var vs /private/var canonicalization surprises.

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/0722ec58116d9a68. Report an issue: GitHub.

Appendix: source

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

    // 根路径 = 有根前缀且没有父目录,覆盖 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(())
}

fn is_filesystem_root(path: &Path) -> bool {
    path.has_root() && path.parent().is_none()
}

/// 规范化到可比较的形态。
///
/// 关键点:**必须两边用同一种方式规范化**。如果待删路径能 canonicalize、而 home
/// 不存在只能走词法,两边就会一个带 `/private` 前缀一个不带(macOS 的 `/var` →
/// `/private/var` 就是这种),比较直接失效、守卫形同虚设。
///

View on GitHub (pinned to b1ed92e5e4)