jdx/mise · error

brew-cask: structured symlink target '{}' already exists

Error message

brew-cask: structured symlink target '{}' already exists

What it means

During a multi-source symlink step, the target exists as a non-directory, is a real file (`target.exists()`), the step is not `force: true`, and the path is not recorded in `targets.previous_symlinks` (links this cask's own transaction created and may replace freely). The installer refuses to overwrite a file it does not own, keeping the destructive protect/remove phase reachable only for owned or forced targets.

Source

Thrown at src/system/packages/brew/cask.rs:3642

            let target = resolve_flight_path_with_context(cask, target, staged_path, appdir)?;
            let sources = flight_symlink_sources(cask, source, *source_glob, staged_path, appdir)?;
            if sources.is_empty() {
                bail!(
                    "brew-cask: structured symlink source '{}' did not match any paths",
                    source.path
                );
            }
            let target_metadata = target.symlink_metadata().ok();
            let target_is_real_dir = target_metadata
                .as_ref()
                .is_some_and(|metadata| metadata.is_dir());
            let target_is_dir = target_is_real_dir || sources.len() > 1;
            if sources.len() > 1 {
                let created_external_directory =
                    target_metadata.is_none() && !target.starts_with(staged_path);
                if target_metadata.is_some() && !target_is_real_dir {
                    if target.exists() && !force && !targets.previous_symlinks.contains(&target) {
                        bail!(
                            "brew-cask: structured symlink target '{}' already exists",
                            target.display()
                        );
                    }
                    targets.protect(&target)?;
                } else if created_external_directory {
                    // Record the absent directory itself so rollback removes
                    // the container after removing the links created below.
                    if let Some(parent) = target.parent() {
                        create_flight_dir_all(parent, *sudo)?;
                    }
                    targets.protect(&target)?;
                }
                create_flight_dir_all(&target, *sudo)?;
                if created_external_directory || targets.previous_directories.contains(&target) {
                    targets.record_installed_directory(target.clone());
                }
            }

View on GitHub (pinned to 6f52dcdf99)

Solutions

  1. Set `force: true` on the symlink step when overwriting foreign files is acceptable
  2. Remove or rename the conflicting file at the target path and retry the install
  3. Run the cask's uninstall flow first so receipts and `previous_symlinks` tracking are consistent
  4. Check whether another installed cask links the same target name and uninstall it

Example fix

# before
symlink(source: "bin/tool", target: "#{prefix}/bin/tool")
# after - allow replacing an unowned existing file
symlink(source: "bin/tool", target: "#{prefix}/bin/tool", force: true)
Defensive patterns

Strategy: validation

Validate before calling

// Before a symlink step, detect unowned existing targets it would refuse
let target = resolve_flight_path_with_context(cask, &step.target, staged_path, appdir)?;
let meta = target.symlink_metadata();
if let Some(m) = &meta {
    let is_foreign_dir_source = sources.len() > 1 && !m.is_dir();
    if (target.exists() || is_foreign_dir_source) && !step.force && !previous_symlinks.contains(&target) {
        return Err(eyre!("target '{}' exists and is not owned by this cask", target.display()));
    }
}

Try / catch

if err.to_string().contains("symlink target") && err.to_string().contains("already exists") {
    // offer: remove the conflicting path, or re-run with force semantics
}

Prevention

When it happens

Trigger: Reinstall where the previous symlink was never recorded in the transaction; a user-created real file at the target path; another package owning a file at the same target location.

Common situations: An earlier install failed mid-transaction so ownership tracking was lost; switching between casks that link the same name; the user ran a manual `ln -s` or copied a file to the target.

Related errors


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