jdx/mise · error

copy_link is not a symbolic link: {}

Error message

copy_link is not a symbolic link: {}

What it means

After confirming the copy_link path exists in the source, mise requires that the entry actually be a symbolic link, since the remote staging step re-materializes symlinks. A regular file or directory given as copy_link fails with this error.

Source

Thrown at src/system/remote.rs:865

    for component in link.parent().unwrap_or_else(|| Path::new("")).components() {
        if let Component::Normal(component) = component {
            parent.push(component);
            let metadata = fs::symlink_metadata(&parent).wrap_err_with(|| {
                format!("copy_link parent does not exist: {}", parent.display())
            })?;
            if metadata.file_type().is_symlink() {
                bail!(
                    "copy_link cannot be nested below a symbolic link: {}",
                    link.display()
                );
            }
        }
    }
    let path = source.join(link);
    let metadata = fs::symlink_metadata(&path)
        .wrap_err_with(|| format!("copy_link does not exist: {}", link.display()))?;
    if !metadata.file_type().is_symlink() {
        bail!("copy_link is not a symbolic link: {}", link.display());
    }
    Ok(())
}

fn materialize_link(source: &Path, staged_source: &Path, link: &Path) -> Result<()> {
    validate_copy_link_path(staged_source, link)
        .wrap_err_with(|| format!("staged copy_link is unsafe: {}", link.display()))?;
    let source_link = source.join(link);
    let staged_link = staged_source.join(link);
    let target = fs::canonicalize(&source_link)
        .wrap_err_with(|| format!("failed to resolve copy_link {}", link.display()))?;
    let parent = staged_link
        .parent()
        .expect("a validated copy_link has a staged parent");
    let original_permissions = make_directory_writable(parent)?;
    let result = (|| {
        crate::file::remove_file(&staged_link)?;
        if target.is_dir() {

View on GitHub (pinned to afd2eddd3a)

Solutions

  1. Restore the entry to be a symbolic link (ln -s <target> <path>)
  2. Remove the copy_link entry if the file is now a real file and no longer needs replication
  3. On Windows/checkouts with symlinks disabled, enable symlink support in git and re-checkout

Example fix

// before: bin/tool is a regular file
copy_link = "bin/tool"
// after
rm bin/tool && ln -s scripts/tool bin/tool
Defensive patterns

Strategy: validation

Validate before calling

function isSymlink(root, rel) {
  try { return fs.lstatSync(path.join(root, rel)).isSymbolicLink(); } catch { return false; }
}

Type guard

const isSymlink = (p) => { try { return fs.lstatSync(p).isSymbolicLink(); } catch { return false; } };

Try / catch

try { await stageRemote(); } catch (e) { if (String(e).includes('copy_link is not a symbolic link')) { recreateSymlinks(); } throw e; }

Prevention

When it happens

Trigger: copy_link names a path that exists but is a regular file or directory, checked via symlink_metadata in validate_copy_link_path before materialize_link runs.

Common situations: Config drift: the entry used to be a symlink but was replaced by a real file after vendoring dependencies, or a checkout option (e.g. git config core.symlinks=false) turned symlinks into plain files.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of jdx/mise@afd2eddd3a (2026-09-09). Data as JSON: /api/errors/e0359af0d141dda3. Report an issue: GitHub.