BigPizzaV3/CodexPlusPlus · critical · anyhow::Error

拒绝递归删除 CODEX_HOME 本身(

Error message

拒绝递归删除 CODEX_HOME 本身({})——这会连同全部会话历史一起丢失

What it means

Thrown by `ensure_safe_recursive_removal` when the deletion target equals the normalized CODEX_HOME directory itself. Recursively deleting CODEX_HOME would destroy all session history and configuration, so the guard refuses it even though the path is not a filesystem root.

Solutions

  1. Pass the specific child directory to remove (e.g. `codex_home.join("sessions/cache-x")`), not `codex_home`
  2. Check your path computation — a join with an empty string returns the base path itself
  3. If the intent really is a full reset, implement an explicit, separately-confirmed reset flow instead of reusing the removal guard

Example fix

// before
ensure_safe_recursive_removal(&codex_home, &codex_home)?; // deletes all history
// after
let session_dir = codex_home.join("sessions").join(&session_id);
ensure_safe_recursive_removal(&session_dir, &codex_home)?;
Defensive patterns

Strategy: validation

Validate before calling

fn is_codex_home_child(target: &Path, home: &Path) -> bool {
    target.starts_with(home) && target != home
}

Prevention

When it happens

Trigger: Passing `codex_home` itself (after normalization, so `~/.codex`, symlinks, or case-variant spellings on Windows all count) as the removal target instead of a child path.

Common situations: A UI or CLI 'reset/cleanup' action that computes the wrong path and targets the whole home directory, variable shadowing where the child path was overwritten with the home path, or symlinked homes making a child resolve to home.

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/2497f77b8205e523. Report an issue: GitHub.

Appendix: source

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

/// 数据丢失不可逆,所以这里加一道与具体触发源无关的兜底。
///
/// 判定在**规范化之后**做,`..`、符号链接、大小写差异都拦得住;同时容忍路径尚不存在
/// (清理临时目录的常见情形,此时用词法规范化比较)。
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(())
}

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

View on GitHub (pinned to b1ed92e5e4)