gitbutlerapp/gitbutler · error

CLI installation failed

Error message

CLI installation failed ({}): {}

What it means

check_cli_install_output validates the result of the macOS AppleScript that installs the CLI symlink with administrator privileges. When the elevated shell script exits non-zero, this error reports the exit status plus the script's trimmed stderr. It wraps real failures of mkdir/rm/ln inside the privileged script (e.g. the destination already exists and symlinkPolicy is not replace, or rm/ln failed).

Solutions

  1. Read the stderr in the error message; it names which command inside the script failed.
  2. If the destination exists as a regular file/directory, remove or move it: `sudo mv /usr/local/bin/but /usr/local/bin/but.bak`, then retry.
  3. Retry the install choosing the Replace symlink policy if you want the existing symlink overwritten.
  4. Check permissions on the target directory: `ls -ld /usr/local/bin` and ensure it is writable by root.
  5. If the authorization dialog itself failed, rerun the install and approve the administrator prompt.

Example fix

// before: destination occupied by a regular file
Error: CLI installation failed (exit status: 1): CLI destination already exists

// after
$ sudo mv /usr/local/bin/but /usr/local/bin/but.bak
$ retry install -> stdout 'gitbutler-cli-installed'
Defensive patterns

Strategy: try-catch

Validate before calling

let dest = "/usr/local/bin/but";
if std::path::Path::new(dest).exists() && !std::path::Path::new(dest).is_symlink() {
    eprintln!("{dest} exists and is not a symlink; move it aside before installing");
}

Try / catch

match install_cli_link_escalated(...) {
    Err(e) => {
        let msg = format!("{e:#}");
        if msg.contains("CLI installation failed") {
            // inspect stderr portion, clear the destination or fix permissions, then retry
        }
    }
    ok => {}
}

Prevention

When it happens

Trigger: install_cli_link_escalated runs the INSTALL_CLI_SCRIPT AppleScript with administrator privileges and the underlying shell command fails: destination exists and is not a replaceable symlink ('CLI destination already exists'), /bin/rm of an old symlink fails, /bin/ln -s -h fails, or /bin/mkdir -p of the target directory fails.

Common situations: A regular file or directory already occupies /usr/local/bin/but so ln fails; the admin script was denied mid-way leaving inconsistent state; a read-only or odd-permission /usr/local/bin; an old GitButler version left an entry the 'keep' symlink policy refuses to touch.

Understand the failure class

Background: "git command failed": what it means when a tool shells out to git and git exits non-zero — this error's family across 21 libraries.

Related errors


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

Appendix: source

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

        do shell script ("/bin/mkdir -p " & targetDirectory & " || exit $?; " & ¬
            "if [ -L " & targetPath & " ] && [ " & symlinkPolicy & " = replace ]; then " & ¬
            "/bin/rm " & targetPath & " || exit $?; " & ¬
            "elif [ -e " & targetPath & " ] || [ -L " & targetPath & " ]; then " & ¬
            "echo 'CLI destination already exists' >&2; exit 1; fi; " & ¬
            "/bin/ln -s -h " & sourcePath & " " & targetPath) ¬
            with prompt "GitButler needs administrator access to install but into /usr/local/bin." ¬
            with administrator privileges
        return "gitbutler-cli-installed"
    on error messageText number errorNumber
        if errorNumber is -128 then return "gitbutler-cli-install-cancelled"
        error messageText number errorNumber
    end try
end run
"#;

#[cfg(any(target_os = "macos", all(test, unix)))]
fn check_cli_install_output(output: std::process::Output) -> anyhow::Result<()> {
    anyhow::ensure!(
        output.status.success(),
        "CLI installation failed ({}): {}",
        output.status,
        String::from_utf8_lossy(&output.stderr).trim()
    );
    if output.stdout == b"gitbutler-cli-install-cancelled\n" {
        return Err(
            anyhow!("Administrator authorization was cancelled").context(ErrorContext::new_static(
                Code::CliInstallCancelled,
                "CLI install cancelled",
            )),
        );
    }
    anyhow::ensure!(
        output.stdout == b"gitbutler-cli-installed\n",
        "Unexpected CLI installer response"
    );
    Ok(())

View on GitHub (pinned to 58e5313667)