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
- Pass the specific child directory to remove (e.g. `codex_home.join("sessions/cache-x")`), not `codex_home`
- Check your path computation — a join with an empty string returns the base path itself
- 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
- Only ever pass codex_home.join(...) children to the removal API
- Beware joins with empty strings — they return the base path itself
- Remember normalization: symlinks/case variants that equal CODEX_HOME are also rejected
- Implement a separate, explicitly confirmed reset flow for full home deletion
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)