{"record":{"id":"2497f77b8205e523","repo":"BigPizzaV3/CodexPlusPlus","slug":"codex-home-2497f7","errorCode":null,"errorMessage":"拒绝递归删除 CODEX_HOME 本身（{}）——这会连同全部会话历史一起丢失","messagePattern":"拒绝递归删除 CODEX_HOME 本身（(.+?)）——这会连同全部会话历史一起丢失","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"critical","filePath":"crates/codex-plus-core/src/codex_home.rs","lineNumber":40,"sourceCode":"/// 数据丢失不可逆，所以这里加一道与具体触发源无关的兜底。\n///\n/// 判定在**规范化之后**做，`..`、符号链接、大小写差异都拦得住；同时容忍路径尚不存在\n/// （清理临时目录的常见情形，此时用词法规范化比较）。\npub fn ensure_safe_recursive_removal(target: &Path, codex_home: &Path) -> anyhow::Result<()> {\n    let target = normalize_for_comparison(target);\n\n    // 根路径 = 有根前缀且没有父目录，覆盖 POSIX 根（`/`）与 Windows 的各种写法\n    // （`C:\\`、`\\\\?\\C:\\`、UNC `\\\\server\\share\\`）。\n    //\n    // 不能只与 `Path::new(\"/\")` 比较：Windows 上 `/` 不是绝对路径，会被 normalize\n    // 成当前盘符根（如 `C:\\`），相等比较拦不住它——也就是说递归删除盘符根本可以\n    // 绕过这道守卫。`has_root()` 这一半也不可省：没有它 `C:` 会被误判成根。\n    if target.as_os_str().is_empty() || is_filesystem_root(&target) {\n        anyhow::bail!(\"拒绝删除文件系统根目录：{}\", target.display());\n    }\n    let home = normalize_for_comparison(codex_home);\n    if target == home {\n        anyhow::bail!(\n            \"拒绝递归删除 CODEX_HOME 本身（{}）——这会连同全部会话历史一起丢失\",\n            target.display()\n        );\n    }\n    if home.starts_with(&target) {\n        anyhow::bail!(\n            \"拒绝删除 CODEX_HOME 的祖先目录 {}（CODEX_HOME = {}）\",\n            target.display(),\n            home.display()\n        );\n    }\n    Ok(())\n}\n\nfn is_filesystem_root(path: &Path) -> bool {\n    path.has_root() && path.parent().is_none()\n}\n","sourceCodeStart":22,"sourceCodeEnd":58,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/crates/codex-plus-core/src/codex_home.rs#L22-L58","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nensure_safe_recursive_removal(&codex_home, &codex_home)?; // deletes all history\n// after\nlet session_dir = codex_home.join(\"sessions\").join(&session_id);\nensure_safe_recursive_removal(&session_dir, &codex_home)?;","handlingStrategy":"validation","validationCode":"fn is_codex_home_child(target: &Path, home: &Path) -> bool {\n    target.starts_with(home) && target != home\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["filesystem","safety-guard","data-loss","codex-home"],"backgroundTag":"path-traversal-blocked","analyzedSha":"b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6","analyzedAt":"2026-09-19T23:35:21.129Z","contentChangedAt":"2026-09-19T23:35:21.129Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}