gitbutlerapp/gitbutler · error

Refusing to replace non-symlink file

Error message

Refusing to replace non-symlink file '{}'

What it means

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.

Solutions

  1. Remove or rename the existing file at destination, then rerun the installer
  2. Allow the policy that permits replacing existing symlinks, or manually replace the file with a symlink to the bundled CLI
  3. Choose a different destination directory that doesn't already contain a 'but' file
  4. Check what owns the file (package manager) and uninstall it via that tool first

Example fix

// before
$ but cli install  # /usr/local/bin/but is a plain file
// after
$ sudo mv /usr/local/bin/but /usr/local/bin/but.bak
$ but cli install
Defensive patterns

Strategy: validation

Validate before calling

if let Ok(md) = std::fs::symlink_metadata(destination) {
    if !md.is_symlink() {
        // pre-check: prompt user or move the file aside before install
    }
}

Prevention

When it happens

Trigger: Installing the CLI where destination already exists as a real file (not a symlink) and the symlink policy doesn't permit replacing it.

Common situations: 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.

Understand the failure class

Background: "already exists" / EEXIST / FileAlreadyExistsException: what the 'file already exists' error means and how to fix it — this error's family across 37 libraries.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@58e5313667 (2026-09-18). Data as JSON: /api/errors/fe69ce95c3575382. Report an issue: GitHub.

Appendix: source

Thrown at crates/but-action/src/cli.rs:123

    /// The install failed because of insufficient privileges - escalation recommended to proceed.
    InstallationRequiresElevatedPrivileges(anyhow::Error),
    /// Any other error - we don't act on this.
    Other(anyhow::Error),
}

#[cfg(any(target_os = "macos", all(test, unix)))]
fn install_cli_link(
    source: &std::path::Path,
    destination: &std::path::Path,
    symlink_policy: ExistingSymlinkPolicy,
) -> Result<(), InstallError> {
    use std::fs;

    validate_cli_source(source).map_err(InstallError::Other)?;
    match fs::symlink_metadata(destination) {
        Ok(metadata) => {
            if !metadata.is_symlink() {
                return Err(InstallError::Other(anyhow::anyhow!(
                    "Refusing to replace non-symlink file '{}'",
                    destination.display()
                )));
            }
            let target = fs::read_link(destination)
                .context("Cannot read existing CLI symlink")
                .map_err(InstallError::Other)?;
            if target == source || matches!(symlink_policy, ExistingSymlinkPolicy::Refuse) {
                return verify_cli_link(source, destination).map_err(InstallError::Other);
            }
            fs::remove_file(destination)
                .context("Cannot remove existing CLI symlink")
                .map_err(escalate_privilege_error_if_permission_denied)?;
        }
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
        Err(err) => {
            return Err(err)
                .context("Cannot inspect CLI installation destination")

View on GitHub (pinned to 58e5313667)