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

  1. Fix the link target to a file path that cannot collide with a directory (usually add the file-name component)
  2. If the directory is disposable, remove or rename it manually after verifying, then reinstall
  3. 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

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


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