jdx/mise · error

existing symbolic link conflicts with adoption

Error message

existing symbolic link conflicts with adoption: {entry}

What it means

When adopting a non-empty destination directory as the global-configuration repository, mise walks every path component of every file in the transferred tree and rejects the adoption if any ancestor of an entry in the destination is a symbolic link. Symlinked ancestors could redirect file writes outside the destination, so adoption is aborted to keep the operation safe and contained.

Solutions

  1. Remove the conflicting symlink(s): `find <destination> -type l` to list them, then `unlink <symlink>` or `rm <symlink>`.
  2. Replace symlinks with real directories, copying content back if needed: `cp -rL <link> <tmp> && rm <link> && mv <tmp> <dir>`.
  3. If a dotfiles manager owns the links, exclude the global-config directory from it or restructure the layout.
  4. Re-run the install after no ancestor of any tracked entry is a symlink.

Example fix

// before
$ ls -l ~/.config/mise/tasks
tasks -> ~/dotfiles/mise-tasks

// after
$ unlink ~/.config/mise/tasks
$ cp -r ~/dotfiles/mise-tasks ~/.config/mise/tasks
Defensive patterns

Strategy: validation

Validate before calling

const fs = require('fs'), path = require('path');
for (const entry of trackedFiles) {
  let dir = dest;
  for (const part of entry.split('/').slice(0, -1)) {
    dir = path.join(dir, part);
    if (fs.lstatSync(dir).isSymbolicLink()) throw new Error('symlinked ancestor blocks adoption: ' + dir);
  }
}

Try / catch

try {
  await installSource(bundle, origin);
} catch (e) {
  if (/symbolic link conflicts/.test(String(e.cause ?? e))) {
    console.error('replace symlinked directories inside the destination with real directories');
  }
  throw e;
}

Prevention

When it happens

Trigger: install/install_source/preview_source (install_at) into a non-empty directory without `.git`, where any parent directory of a tracked entry (e.g. `<dest>/config` when adopting file `config/settings.toml`) is a symlink on disk.

Common situations: Users symlink directories like `~/.config/mise/tasks` to a dotfiles-managed location; a dotfiles manager (GNU Stow, chezmoi) created symlinks inside the destination; `settings` or subdirectories were symlinked for sharing across machines.

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 jdx/mise@533346cc37 (2026-09-17). Data as JSON: /api/errors/411fc047211b5b40. Report an issue: GitHub.

Appendix: source

Thrown at src/system/remote_repository.rs:329

                    &format!("refs/remotes/origin/{branch}"),
                    revision,
                ],
            )?;
        } else if dry_run {
            miseprintln!(
                "Would keep the existing checkout of {origin} at {shown}; --update fast-forwards it"
            );
        }
        return Ok(destination.to_path_buf());
    }
    let nonempty = destination.exists() && destination.read_dir()?.next().is_some();
    if nonempty {
        for entry in entries.split('\0').filter(|s| !s.is_empty()) {
            let mut ancestor = destination.to_path_buf();
            for component in Path::new(entry).components() {
                ancestor.push(component);
                if ancestor.is_symlink() {
                    bail!("existing symbolic link conflicts with adoption: {entry}");
                }
            }
            let existing = destination.join(entry);
            if existing.exists()
                && (checkout.join(entry).is_symlink()
                    || !existing.is_file()
                    || std::fs::read(&existing)? != std::fs::read(checkout.join(entry))?)
            {
                bail!("existing file conflicts with adoption: {entry}");
            }
        }
        if dry_run {
            let new_files = entries
                .split('\0')
                .filter(|s| !s.is_empty())
                .filter(|entry| !destination.join(entry).exists())
                .count();
            miseprintln!(

View on GitHub (pinned to 533346cc37)