jdx/mise · error
brew-cask: refusing to replace structured symlink directory
Error message
brew-cask: refusing to replace structured symlink directory '{}' What it means
The installer refuses to replace a real directory with a symlink. When the final link path's metadata says it is a directory, the step bails unconditionally: `force` does not override it, because swapping a directory for a link is destructive and almost always a definition bug rather than an overwrite case.
Source
Thrown at src/system/packages/brew/cask.rs:3674
create_flight_dir_all(&target, *sudo)?;
if created_external_directory || targets.previous_directories.contains(&target) {
targets.record_installed_directory(target.clone());
}
}
for source in sources {
let link = if target_is_dir {
let source_name = Path::new(&source).file_name().ok_or_else(|| {
eyre!("brew-cask: structured symlink source has no file name")
})?;
target.join(source_name)
} else {
target.clone()
};
let external = !link.starts_with(staged_path);
let link_metadata = link.symlink_metadata().ok();
if let Some(metadata) = &link_metadata {
if metadata.is_dir() {
bail!(
"brew-cask: refusing to replace structured symlink directory '{}'",
link.display()
);
}
if link.exists() && !force && !targets.previous_symlinks.contains(&link) {
bail!(
"brew-cask: structured symlink target '{}' already exists",
link.display()
);
}
if external {
targets.protect(&link)?;
} else if metadata.file_type().is_symlink() {
file::remove_file(&link)?;
} else {
file::remove_all(&link)?;
}
}View on GitHub (pinned to 6f52dcdf99)
Solutions
- Fix the link target to a file path that cannot collide with a directory (usually add the file-name component)
- If the directory is disposable, remove or rename it manually after verifying, then reinstall
- If the directory is the intended container, use it as the symlink step's target so links are created inside it rather than replacing it
Example fix
# before - target omits the file name and collides with a directory
symlink(source: "*.app/Contents/MacOS/tool", target: "#{prefix}/tool", source_glob: true)
# after - link into the directory explicitly by file name
symlink(source: "tool", target: "#{prefix}/bin/tool") Defensive patterns
Strategy: validation
Validate before calling
// Refuse early if any planned link path lands on a real directory
for source in &sources {
let link = if target_is_dir { target.join(source.file_name()?) } else { target.clone() };
if link.symlink_metadata().is_some_and(|m| m.is_dir()) {
return Err(eyre!("link path '{}' is a real directory; fix the target", link.display()));
}
} Try / catch
if err.to_string().contains("refusing to replace structured symlink directory") {
// never auto-force this one: it is a safety guard; fix the DSL target instead
} Prevention
- Always include the file-name component in symlink targets
- Check the target directory for name collisions before linking
- Never try to bypass this check with force; it intentionally ignores force
When it happens
Trigger: A symlink target (or `target/<source file name>` in the multi-source case) lands on an existing real directory: linking a binary whose name matches a directory in the target dir, or linking over an `.app` bundle directory.
Common situations: Name collision between an artifact and a system or other-package directory; DSL target missing its file-name component; leftover directory from an artifact that changed type between versions.
Related errors
- brew-cask: refusing to replace structured symlink directory
- brew-cask: staged symlink path escaped extraction root: {}
- brew-cask: refusing generic artifact source outside the extr
- brew-cask: refusing to restore flight target through a chang
- brew-cask: structured symlink source '{}' did not match any
AI-assisted analysis of jdx/mise@6f52dcdf99 (2026-08-22).
Data as JSON: /api/errors/2dce096d6a6da339.
Report an issue: GitHub.