gitbutlerapp/gitbutler · error

Refusing to replace ' ', which points to ' ' instead of

Error message

Refusing to replace '{}', which points to '{}' instead of '{}'

What it means

During GitButler CLI installation on macOS, verify_cli_link reads the existing symlink at the destination (e.g. /usr/local/bin/but) and refuses to proceed when it points somewhere other than the expected CLI executable. This is a safety guard so the installer never silently clobbers a symlink the user (or another tool) installed to a different binary. The message includes the destination, the unexpected current target, and the expected target.

Solutions

  1. Remove the stale symlink manually: `sudo rm /usr/local/bin/but`, then retry the install.
  2. Check where it currently points with `ls -l /usr/local/bin/but`; if it points to an existing binary you still want, move it aside instead of deleting it.
  3. If it is a dangling link to an old app bundle, reinstalling via the GitButler app's CLI-install flow (or auto_fix_broken_but_cli_symlink) replaces it.
  4. If the symlink is correct but paths differ only by form (relative vs absolute), recreate it with an absolute target: `sudo ln -sf <absolute-path-to-but> /usr/local/bin/but`.

Example fix

// before: stale symlink blocks install
$ but install
Error: Refusing to replace '/usr/local/bin/but', which points to '/Applications/GitButler-old.app/...' instead of '/Applications/GitButler.app/...'

// after
$ sudo rm /usr/local/bin/but
$ but install  # succeeds
Defensive patterns

Strategy: validation

Validate before calling

let dest = std::path::Path::new("/usr/local/bin/but");
if let Ok(target) = std::fs::read_link(dest) {
    if target != expected_source {
        eprintln!("{} points at {}; remove or move it before installing", dest.display(), target.display());
    }
}

Prevention

When it happens

Trigger: Calling install_cli_link or install_cli_link_escalated when the destination path already contains a symlink whose read_link() target differs from the resolved `but` CLI source path (e.g. a symlink to a previously installed version in an uninstalled app bundle, or a user-made symlink to their own binary).

Common situations: A stale symlink left behind after uninstalling or moving the GitButler app (a dangling link to an old .app bundle path); the user previously linked their own `but`/other tool at the same path; a version upgrade changed the bundled CLI source location so the old link no longer matches.

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/1be8c1db0b5b715d. Report an issue: GitHub.

Appendix: source

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

#[cfg(any(target_os = "macos", all(test, unix)))]
fn validate_cli_source(source: &std::path::Path) -> anyhow::Result<()> {
    use std::os::unix::fs::PermissionsExt;

    anyhow::ensure!(source.is_absolute(), "CLI source path must be absolute");
    let metadata = std::fs::metadata(source)
        .with_context(|| format!("Cannot access CLI executable at '{}'", source.display()))?;
    anyhow::ensure!(
        metadata.is_file() && metadata.permissions().mode() & 0o111 != 0,
        "CLI source '{}' must be an executable file",
        source.display()
    );
    Ok(())
}

#[cfg(any(target_os = "macos", all(test, unix)))]
fn verify_cli_link(source: &std::path::Path, destination: &std::path::Path) -> anyhow::Result<()> {
    let target = std::fs::read_link(destination).context("Cannot read installed CLI symlink")?;
    anyhow::ensure!(
        target == source,
        "Refusing to replace '{}', which points to '{}' instead of '{}'",
        destination.display(),
        target.display(),
        source.display()
    );
    Ok(())
}

#[cfg(any(target_os = "macos", all(test, unix)))]
const INSTALL_CLI_SCRIPT: &str = r#"
on run argv
    set sourcePath to quoted form of (item 1 of argv)
    set targetPath to quoted form of (item 2 of argv)
    set targetDirectory to quoted form of (item 3 of argv)
    set symlinkPolicy to quoted form of (item 4 of argv)
    try
        do shell script ("/bin/mkdir -p " & targetDirectory & " || exit $?; " & ¬

View on GitHub (pinned to 58e5313667)