jdx/mise · error · eyre::Report

refusing to remove non-link: {}

Error message

refusing to remove non-link: {}

What it means

Unix implementation of `remove_symlink_or_junction` (src/file.rs:1201): before unlinking, `dir_link_target` must confirm the path actually is a symlink (to a directory). If it isn't — a regular file or a real directory sits at the path — mise refuses, because `fs::remove_file` must never delete real directory trees. The doc comment notes a POSIX race is still possible; callers must keep the parent dir protected from untrusted writers.

Source

Thrown at src/file.rs:1210

    };
    Ok(Some(resolve_relative_link_target(link, target)))
}

fn resolve_relative_link_target(link: &Path, target: PathBuf) -> PathBuf {
    if target.is_absolute() {
        target
    } else {
        link.parent().unwrap_or(link).join(target)
    }
}

#[cfg(unix)]
pub fn remove_symlink_or_junction(link: &Path) -> Result<()> {
    // POSIX has no standard unlink-by-handle operation, so a concurrent replacement can still
    // occur between this check and remove_file. The latter cannot remove directories; callers
    // must keep the parent directory protected from untrusted writers.
    if dir_link_target(link)?.is_none() {
        bail!("refusing to remove non-link: {}", display_path(link));
    }
    fs::remove_file(link)
        .wrap_err_with(|| format!("failed to remove symlink: {}", display_path(link)))
}

#[cfg(windows)]
pub fn remove_symlink_or_junction(link: &Path) -> Result<()> {
    let link_handle = open_link_for_removal(link)?;
    remove_open_link(link_handle)
        .wrap_err_with(|| format!("failed to remove link or junction: {}", display_path(link)))
}

#[cfg(windows)]
fn open_link_for_removal(link: &Path) -> Result<File> {
    use std::os::windows::fs::OpenOptionsExt;
    use std::os::windows::io::AsRawHandle;
    use windows_sys::Win32::Storage::FileSystem::{
        DELETE, FILE_ATTRIBUTE_TAG_INFO, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT,

View on GitHub (pinned to 9dcfcaa0dc)

Solutions

  1. Inspect the path: `ls -la <path>` — determine whether a real file/directory now occupies a spot mise expects to own as a symlink
  2. If the real directory is yours, move its contents to safety, then remove it manually (`rm -rf` only after verifying)
  3. Reinstall the affected tool version so mise recreates its links: `mise install -f <tool>@<version>`
  4. Stop placing real files/dirs on paths mise manages as symlinks; use mise aliases instead

Example fix

# before: installs/node/22 is a real dir, not a symlink
$ mv ~/.local/share/mise/installs/node/22 /tmp/node-22-backup
$ mise install -f node@22
# after: mise recreates and owns the link/dir layout
Defensive patterns

Strategy: type-guard

Validate before calling

# confirm the path is a symlink before letting mise replace it
[ -L "$path" ] && echo link || { echo "$path is a real file/dir — resolve manually" >&2; exit 1; }

Type guard

fn is_removable_link(p: &std::path::Path) -> bool {
    p.symlink_metadata()
        .map(|m| m.file_type().is_symlink())
        .unwrap_or(false)
}

Try / catch

Catch `refusing to remove non-link`, inspect the path with `ls -la`, and only after confirming the real file/dir is not mise-managed data, remove it manually and re-run the install command.

Prevention

When it happens

Trigger: mise replacing an install/version symlink (e.g. under `~/.local/share/mise/installs/<tool>/<version>` or a shim link) when that path has been replaced by a real file or directory — a user copied a real install over the link location, or a tool wrote through the link and replaced it.

Common situations: Users 'fixing' broken installs by `cp -r`-ing a real directory onto mise's symlink path; backups/restores that did not preserve symlink-ness; interrupted installs leaving directories where links belong; third-party version managers writing into the same paths.

Related errors


AI-assisted analysis of jdx/mise@9dcfcaa0dc (2026-08-17). Data as JSON: /api/errors/2f2e01232c6278a7. Report an issue: GitHub.