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
- Set `force: true` on the symlink step when overwriting foreign files is acceptable
- Remove or rename the conflicting file at the target path and retry the install
- Run the cask's uninstall flow first so receipts and `previous_symlinks` tracking are consistent
- 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
- Use `force: true` for link names the cask legitimately owns
- Complete uninstalls before reinstalling so ownership receipts stay accurate
- Avoid link names that collide with system or other-package files
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
- brew-cask: structured symlink source '{}' did not match any
- brew-cask:{}: Homebrew owns this cask; remove it with Homebr
- brew-cask:{}: conflicts with installed cask {}
- brew-cask: cannot adopt '{}': existing artifact is not ident
- brew-cask: staged symlink path escaped extraction root: {}
AI-assisted analysis of jdx/mise@6f52dcdf99 (2026-08-22).
Data as JSON: /api/errors/99a1fc333e7ca936.
Report an issue: GitHub.