{"record":{"id":"fe69ce95c3575382","repo":"gitbutlerapp/gitbutler","slug":"refusing-to-replace-non-symlink-file","errorCode":null,"errorMessage":"Refusing to replace non-symlink file '{}'","messagePattern":"Refusing to replace non-symlink file '(.+?)'","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but-action/src/cli.rs","lineNumber":123,"sourceCode":"    /// The install failed because of insufficient privileges - escalation recommended to proceed.\n    InstallationRequiresElevatedPrivileges(anyhow::Error),\n    /// Any other error - we don't act on this.\n    Other(anyhow::Error),\n}\n\n#[cfg(any(target_os = \"macos\", all(test, unix)))]\nfn install_cli_link(\n    source: &std::path::Path,\n    destination: &std::path::Path,\n    symlink_policy: ExistingSymlinkPolicy,\n) -> Result<(), InstallError> {\n    use std::fs;\n\n    validate_cli_source(source).map_err(InstallError::Other)?;\n    match fs::symlink_metadata(destination) {\n        Ok(metadata) => {\n            if !metadata.is_symlink() {\n                return Err(InstallError::Other(anyhow::anyhow!(\n                    \"Refusing to replace non-symlink file '{}'\",\n                    destination.display()\n                )));\n            }\n            let target = fs::read_link(destination)\n                .context(\"Cannot read existing CLI symlink\")\n                .map_err(InstallError::Other)?;\n            if target == source || matches!(symlink_policy, ExistingSymlinkPolicy::Refuse) {\n                return verify_cli_link(source, destination).map_err(InstallError::Other);\n            }\n            fs::remove_file(destination)\n                .context(\"Cannot remove existing CLI symlink\")\n                .map_err(escalate_privilege_error_if_permission_denied)?;\n        }\n        Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}\n        Err(err) => {\n            return Err(err)\n                .context(\"Cannot inspect CLI installation destination\")","sourceCodeStart":105,"sourceCodeEnd":141,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/58e5313667b857ef39a730e380af31816a7b1768/crates/but-action/src/cli.rs#L105-L141","documentation":"install_cli_link installs the CLI by creating a symlink at destination. Safety policy: if the destination exists and is a regular file (not a symlink), it refuses to overwrite it, returning InstallError::Other wrapping 'Refusing to replace non-symlink file {}'. This prevents clobbering a user's own binary.","triggerScenarios":"Installing the CLI where destination already exists as a real file (not a symlink) and the symlink policy doesn't permit replacing it.","commonSituations":"A previous manual copy of the binary at /usr/local/bin/but; another tool (e.g. Homebrew, cargo install) owns the path; leftovers from an old non-symlink installer.","solutions":["Remove or rename the existing file at destination, then rerun the installer","Allow the policy that permits replacing existing symlinks, or manually replace the file with a symlink to the bundled CLI","Choose a different destination directory that doesn't already contain a 'but' file","Check what owns the file (package manager) and uninstall it via that tool first"],"exampleFix":"// before\n$ but cli install  # /usr/local/bin/but is a plain file\n// after\n$ sudo mv /usr/local/bin/but /usr/local/bin/but.bak\n$ but cli install","handlingStrategy":"validation","validationCode":"if let Ok(md) = std::fs::symlink_metadata(destination) {\n    if !md.is_symlink() {\n        // pre-check: prompt user or move the file aside before install\n    }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Check symlink_metadata at destination before installing","Uninstall package-manager-owned 'but' binaries first","Back up any existing plain file before removal","Prefer dedicated install dirs to avoid collisions"],"tags":["cli","install","symlink","file-conflict"],"backgroundTag":"file-already-exists","analyzedSha":"58e5313667b857ef39a730e380af31816a7b1768","analyzedAt":"2026-09-18T06:50:32.052Z","contentChangedAt":"2026-09-18T06:50:32.052Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}