jdx/mise · error

target symlink points to {}, use --force to remove it

Error message

target symlink points to {}, use --force to remove it

What it means

For a `symlink`-mode entry, unapply removes the target only when the link provably belongs to mise: its `read_link` destination equals the configured `source`, or both resolve to the same file (`points_at_same_file`). A symlink pointing elsewhere is treated as foreign — the error names the current destination — and `--force` is required to remove it.

Source

Thrown at src/system/files.rs:1537

    let mut paths = IndexMap::<PathBuf, ()>::new();
    let mut cleanup_empty_dirs = false;
    let mut conditional = false;
    let mut clear_symlink_each_state = false;
    match req.mode {
        // A Windows file link that came out as a copy is planned by content further down; one
        // that is a real symlink belongs here, where the link target is what identifies it as
        // ours. `plan_expected_content` requires a non-symlink, so routing a symlink there
        // would make unapply demand `--force`.
        FileMode::Symlink
            if !(cfg!(windows) && req.source.is_file() && !req.target.is_symlink()) =>
        {
            if req.target.is_symlink() {
                let dest = std::fs::read_link(&req.target)?;
                if opts.force || dest == req.source || points_at_same_file(&req.target, &req.source)
                {
                    paths.insert(req.target.clone(), ());
                } else {
                    bail!(
                        "target symlink points to {}, use --force to remove it",
                        dest.display_user()
                    );
                }
            } else if req.target.exists() {
                if opts.force {
                    paths.insert(req.target.clone(), ());
                } else {
                    bail!("target is not the managed symlink, use --force to remove it");
                }
            }
        }
        FileMode::SymlinkEach if !cfg!(windows) => {
            clear_symlink_each_state = symlink_each_state_path(req).exists();
            if req.target.is_symlink() || (req.target.exists() && !req.target.is_dir()) {
                if opts.force {
                    paths.insert(req.target.clone(), ());
                } else {

View on GitHub (pinned to 6f52dcdf99)

Solutions

  1. Run `readlink <target>` and compare with the entry's `source` in mise.toml; fix whichever is wrong, or re-apply so mise recreates the link
  2. If the link is genuinely yours and should go, re-run with `--force`
  3. If the link belongs to another tool, remove the `[dotfiles]` entry instead of forcing deletion

Example fix

# before: target points elsewhere than the configured source
$ readlink ~/.vimrc
/home/me/other/vimrc          # mise.toml says source = "~/dotfiles/vimrc"
$ mise bootstrap dotfiles unapply
Error: target symlink points to /home/me/other/vimrc, use --force to remove it

# after: source of truth is ~/dotfiles/vimrc -> re-apply first
$ mise bootstrap dotfiles apply   # recreates the correct symlink
$ mise bootstrap dotfiles unapply
Defensive patterns

Strategy: validation

Validate before calling

# Pre-check: every symlink-mode entry's target must point at its source.
check() {
  src=$(realpath "$1"); tgt_link=$(readlink "$2" 2>/dev/null)
  [ "$tgt_link" ] && [ "$(realpath "$2")" = "$src" ] || echo "MISMATCH: $2 -> $tgt_link (want $src)"
}
check ~/dotfiles/vimrc ~/.vimrc

Type guard

fn is_foreign_symlink(err: &miette::Report) -> bool {
    err.to_string()
        .starts_with("target symlink points to")
}

Prevention

When it happens

Trigger: `unapply` of an entry whose target is a symlink whose read_link destination differs from `req.source` and does not resolve to the same file: the link was re-pointed manually, or the entry's `source` path in mise.toml changed after the link was created.

Common situations: Another dotfile manager owns the link; user re-pointed ~/.vimrc at a different config; source renamed in the repo without re-applying; absolute vs relative path churn in config.

Related errors


AI-assisted analysis of jdx/mise@6f52dcdf99 (2026-08-22). Data as JSON: /api/errors/ebf08aecb0ee1ae5. Report an issue: GitHub.