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
- Inspect the target being deleted — it must be a leaf directory BELOW CODEX_HOME (or unrelated to it), never a parent
- Check the CODEX_HOME value (env var or profile setting) for mistakes like ~/ or a drive/mount root
- Verify how the target path was constructed; remove any `..` components or symlinked segments that resolve it above CODEX_HOME
- 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
- Never construct deletion targets by trimming/parenting CODEX_HOME paths
- Canonicalize targets before any recursive delete
- Keep CODEX_HOME as a dedicated directory, not directly under directories you clean
- Test cleanup code with CODEX_HOME set to unusual values (~/, roots, symlinked paths)
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
- 拒绝删除文件系统根目录:
- 拒绝递归删除 CODEX_HOME 本身(
- error.to_string()
- Unexpected monitor lock type
- Codex CLI 路径指向的是目录而不是可执行文件:
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)