gitbutlerapp/gitbutler · warning · anyhow::Error

Refusing to install symlink onto existing non-symlink at '{U

Error message

Refusing to install symlink onto existing non-symlink at '{UNIX_LINK_PATH}'

What it means

but-action's CLI installer manages /usr/local/bin/but strictly as a symlink to the built binary. If something already exists at that path and is not a symlink (a copied binary, a Homebrew-owned file), it refuses rather than clobber a file it does not own (crates/but-action/src/cli.rs:31, UNIX_LINK_PATH = "/usr/local/bin/but").

Source

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

const UNIX_LINK_PATH: &str = "/usr/local/bin/but";

pub enum InstallMode {
    AllowPrivilegeElevation,
    CurrentUserOnly,
}

pub fn do_install_cli(mode: InstallMode) -> anyhow::Result<()> {
    let cli_path = get_cli_path()?;
    #[cfg(windows)]
    {
        return install_cli_windows(cli_path);
    }

    match std::fs::symlink_metadata(UNIX_LINK_PATH) {
        Ok(md) => {
            if !md.is_symlink() {
                bail!(
                    "Refusing to install symlink onto existing non-symlink at '{UNIX_LINK_PATH}'"
                );
            }
            let current_link = std::fs::read_link(UNIX_LINK_PATH)
                .context(format!("error reading existing link: {UNIX_LINK_PATH}"))?;
            if current_link == cli_path {
                return Ok(());
            }
            ensure_cli_path_exists_prior_to_link(&cli_path)?;
            #[cfg(not(windows))]
            if std::fs::remove_file(UNIX_LINK_PATH)
                .and_then(|_| std::os::unix::fs::symlink(&cli_path, UNIX_LINK_PATH))
                .is_ok()
            {
                return Ok(());
            }
        }
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => {

View on GitHub (pinned to caf1f223d3)

Solutions

  1. Inspect what is there: `ls -l /usr/local/bin/but` and `file /usr/local/bin/but`
  2. If it is an old copy of the but CLI you no longer need, remove it (`rm /usr/local/bin/but`) or rename it aside, then retry the install
  3. If another package owns the path, keep that install and skip the symlink install, or choose a different link location on your PATH

Example fix

# before: /usr/local/bin/but is a regular file -> install refuses
$ ls -l /usr/local/bin/but
-rwxr-xr-x 1 root staff 41324672 Jan 1 10:00 /usr/local/bin/but

# after: remove the copy and re-run install (from the app or CLI)
$ rm /usr/local/bin/but
$ # retry install -> creates symlink to the built binary
$ ls -l /usr/local/bin/but
lrwxr-xr-x 1 root staff 9 Aug 20 11:00 /usr/local/bin/but -> .../target/tauri/but
Defensive patterns

Strategy: validation

Validate before calling

// Check the link path before invoking install
let md = std::fs::symlink_metadata("/usr/local/bin/but");
if let Ok(md) = &md {
    if md.exists() && !md.is_symlink() {
        // resolve manually: remove/rename the foreign file, then install
    }
}

Type guard

fn link_path_is_free_or_symlink() -> bool {
    match std::fs::symlink_metadata("/usr/local/bin/but") {
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => true,
        Ok(md) => md.is_symlink(),
        Ok(_) => false,
    }
}

Prevention

When it happens

Trigger: Running the desktop app's 'install CLI' action (do_install_cli) when /usr/local/bin/but already exists as a regular file - e.g. an older GitButler release copied the binary there, or another distribution method placed a real file at that path.

Common situations: Upgrading from an old GitButler version that copied instead of symlinked; /usr/local/bin managed by Homebrew on macOS; a manually copied but binary from a release tarball.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20). Data as JSON: /api/errors/b14eb270e86a97a5. Report an issue: GitHub.