{"record":{"id":"2dce096d6a6da339","repo":"jdx/mise","slug":"brew-cask-refusing-to-replace-structured-symlink","errorCode":null,"errorMessage":"brew-cask: refusing to replace structured symlink directory '{}'","messagePattern":"brew-cask: refusing to replace structured symlink directory '(.+?)'","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/packages/brew/cask.rs","lineNumber":3674,"sourceCode":"                create_flight_dir_all(&target, *sudo)?;\n                if created_external_directory || targets.previous_directories.contains(&target) {\n                    targets.record_installed_directory(target.clone());\n                }\n            }\n            for source in sources {\n                let link = if target_is_dir {\n                    let source_name = Path::new(&source).file_name().ok_or_else(|| {\n                        eyre!(\"brew-cask: structured symlink source has no file name\")\n                    })?;\n                    target.join(source_name)\n                } else {\n                    target.clone()\n                };\n                let external = !link.starts_with(staged_path);\n                let link_metadata = link.symlink_metadata().ok();\n                if let Some(metadata) = &link_metadata {\n                    if metadata.is_dir() {\n                        bail!(\n                            \"brew-cask: refusing to replace structured symlink directory '{}'\",\n                            link.display()\n                        );\n                    }\n                    if link.exists() && !force && !targets.previous_symlinks.contains(&link) {\n                        bail!(\n                            \"brew-cask: structured symlink target '{}' already exists\",\n                            link.display()\n                        );\n                    }\n                    if external {\n                        targets.protect(&link)?;\n                    } else if metadata.file_type().is_symlink() {\n                        file::remove_file(&link)?;\n                    } else {\n                        file::remove_all(&link)?;\n                    }\n                }","sourceCodeStart":3656,"sourceCodeEnd":3692,"githubUrl":"https://github.com/jdx/mise/blob/6f52dcdf99e282ef7a7db68c81301fa4618d0f79/src/system/packages/brew/cask.rs#L3656-L3692","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"# before - target omits the file name and collides with a directory\nsymlink(source: \"*.app/Contents/MacOS/tool\", target: \"#{prefix}/tool\", source_glob: true)\n# after - link into the directory explicitly by file name\nsymlink(source: \"tool\", target: \"#{prefix}/bin/tool\")","handlingStrategy":"validation","validationCode":"// Refuse early if any planned link path lands on a real directory\nfor source in &sources {\n    let link = if target_is_dir { target.join(source.file_name()?) } else { target.clone() };\n    if link.symlink_metadata().is_some_and(|m| m.is_dir()) {\n        return Err(eyre!(\"link path '{}' is a real directory; fix the target\", link.display()));\n    }\n}","typeGuard":null,"tryCatchPattern":"if err.to_string().contains(\"refusing to replace structured symlink directory\") {\n    // never auto-force this one: it is a safety guard; fix the DSL target instead\n}","preventionTips":["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"],"tags":["brew-cask","symlink","directory-conflict","safety-guard"],"backgroundTag":"symlink-over-directory","analyzedSha":"6f52dcdf99e282ef7a7db68c81301fa4618d0f79","analyzedAt":"2026-08-22T10:14:23.840Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}