gitbutlerapp/gitbutler · warning

A 'but' binary already exists at

Error message

A 'but' binary already exists at {} (not a symlink)

What it means

While wiring up the 'but' CLI symlink in the bin directory, install_app on macOS finds an existing non-symlink file at the symlink target path. It warns that a real 'but' binary already exists and that it will be moved aside (to but.backup.<timestamp>) to preserve the user's file before creating the symlink.

Solutions

  1. Let the installer proceed: your existing binary is preserved as but.backup.<timestamp> in the same directory.
  2. If you want to keep your custom build authoritative, remove it from the installer's bin dir or point the installer at a different bin directory.
  3. Delete the stale binary yourself first if you no longer need it (rm <bin_dir>/but) and re-run the installer.
  4. Check whether a package manager owns the binary (e.g. dpkg -S / brew list) and uninstall it there to avoid the collision recurring.
Defensive patterns

Strategy: validation

Validate before calling

# Detect a non-symlink 'but' in your bin dir before installing
import os
def but_is_symlink(bin_dir):
    p = os.path.join(bin_dir, 'but')
    return os.path.islink(p) or not os.path.exists(p)

Try / catch

// Move aside any real file before creating the symlink
if path.is_file() && !path.symlink_metadata().map(|m| m.file_type().is_symlink()).unwrap_or(false) {
    let backup = bin_dir.join(format!("but.backup.{}", timestamp()));
    fs::rename(&path, &backup)?;
}

Prevention

When it happens

Trigger: Raised in install_app (download_and_install_app) when the path bin_dir/but exists but is not a symlink — i.e. a previous install copied a real binary there, or the user placed a custom build named 'but' in their PATH directory.

Common situations: Upgrading from an install method that copied the binary instead of symlinking; a locally built 'but' binary sitting in ~/.local/bin or /usr/local/bin; package-manager-managed 'but' binary colliding with the installer's symlink.

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/3ac7058d1d7ba5a7. Report an issue: GitHub.

Appendix: source

Thrown at crates/but-installer/src/install_macos.rs:126

    }

    // Create bin directory
    let bin_dir = home_dir.join(".local/bin");
    fs::create_dir_all(&bin_dir)?;

    // Check for existing 'but' and detect channel switching
    let but_symlink = bin_dir.join("but");
    let but_new = bin_dir.join("but.new");

    if but_symlink.exists() && !but_symlink.is_symlink() {
        // 'but' exists but is not a symlink
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();
        let timestamp = format!("{now}");
        let but_backup = bin_dir.join(format!("but.backup.{timestamp}"));
        warn(&format!(
            "A 'but' binary already exists at {} (not a symlink)",
            but_symlink.display()
        ));
        warn(&format!(
            "Moving it to {} to preserve your existing file",
            but_backup.display()
        ));
        fs::rename(&but_symlink, &but_backup)?;
        info(&format!(
            "Your original 'but' has been saved to: {}",
            but_backup.display()
        ));
    } else if but_symlink.is_symlink() {
        let existing_target = fs::read_link(&but_symlink)?;
        let existing_target_str = existing_target.to_string_lossy();

        // Detect channel switching
        let previous_channel = if existing_target_str.contains("Nightly") {

View on GitHub (pinned to 58e5313667)