BigPizzaV3/CodexPlusPlus · critical · std::io::Error (PermissionDenied)
error.to_string()
Error message
error.to_string()
What it means
In remove_owned_data, before recursively deleting the uninstall directory, the code guards against removing CODEX_HOME or any of its ancestors (issue #2146). If ensure_safe_recursive_removal rejects the path, the guard's error text is wrapped into an io::Error with PermissionDenied kind via error.to_string().
Solutions
- Pass the actual owned data directory (the app's install/data dir), not CODEX_HOME or an ancestor
- Check the CODEX_HOME environment variable / default_codex_home_dir() to confirm which paths are protected
- Log the guard error to see exactly which path relationship triggered the refusal
- If the target legitimately contains home but is not the home itself, delete subdirectories individually instead of the ancestor
Example fix
// before
remove_owned_data(&std::env::var("HOME").unwrap()); // resolves to ancestor of CODEX_HOME
// after
let data_dir = install_root.join("owned-data");
remove_owned_data(&data_dir)?; Defensive patterns
Strategy: validation
Validate before calling
const home = process.env.CODEX_HOME || defaultCodexHome();
const rel = path.relative(home, targetDir);
if (rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel))) {
throw new Error('refusing to delete CODEX_HOME or its ancestor');
} Type guard
const isSafeDeleteTarget = (target: string, home: string): boolean =>
path.relative(home, path.resolve(target)).startsWith('..'); Try / catch
if let Err(e) = remove_owned_data(&dir) {
if e.kind() == std::io::ErrorKind::PermissionDenied {
eprintln!("delete refused by safety guard: {e}");
// do not retry; fix the target path
}
} Prevention
- Always pass the app's owned data dir, never CODEX_HOME or its ancestors
- Verify resolved (canonical) paths, not env strings, before deleting
- Test uninstall paths with a fake CODEX_HOME in CI
When it happens
Trigger: Calling remove_owned_data with a dir that equals or is a parent/ancestor of the default codex home directory (e.g. the user's home dir, or CODEX_HOME itself), causing the safety guard to refuse the recursive delete.
Common situations: Uninstaller configured with the wrong install dir (empty or '/'-like path that is an ancestor of CODEX_HOME); environment misconfiguration where CODEX_HOME resolves to a broad directory; passing the wrong variable to the uninstall routine.
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/2d5fb1fa2e047d76.
Report an issue: GitHub.
Appendix: source
Thrown at crates/codex-plus-core/src/install/mod.rs:132
windows::build_windows_entrypoint_plan(options)
}
pub fn build_macos_app_bundle(options: &InstallOptions, manager: bool) -> MacosAppBundle {
macos::build_app_bundle(options, manager)
}
pub fn remove_owned_data() -> std::io::Result<()> {
let dir = crate::paths::default_app_state_dir();
if !dir.exists() {
return Ok(());
}
// 卸载流程会递归删除,路径来自环境/推导,先过一道"不许删 CODEX_HOME 及其祖先"
// 的兜底(#2146)。守卫只在这条路径确实指向 home 时才会拒绝,正常卸载不受影响。
if let Err(error) = crate::codex_home::ensure_safe_recursive_removal(
&dir,
&crate::codex_home::default_codex_home_dir(),
) {
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
error.to_string(),
));
}
std::fs::remove_dir_all(dir)?;
Ok(())
}
pub fn default_install_root() -> Option<PathBuf> {
#[cfg(windows)]
{
return crate::windows_integration::desktop_dir().or_else(|| {
directories::UserDirs::new().and_then(|dirs| dirs.desktop_dir().map(PathBuf::from))
});
}
#[cfg(target_os = "macos")]
{View on GitHub (pinned to b1ed92e5e4)