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

  1. Pass the actual owned data directory (the app's install/data dir), not CODEX_HOME or an ancestor
  2. Check the CODEX_HOME environment variable / default_codex_home_dir() to confirm which paths are protected
  3. Log the guard error to see exactly which path relationship triggered the refusal
  4. 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

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)