gitbutlerapp/gitbutler · error

CLI source ' ' must be an executable file

Error message

CLI source '{}' must be an executable file

What it means

validate_cli_source also requires the source path to point to an existing, executable file: metadata must succeed, the entry must be a regular file, and its permission bits must include any execute bit (0o111). Otherwise it errors with 'CLI source {} must be an executable file'.

Solutions

  1. chmod +x the CLI binary at the source path
  2. Verify the path points to the actual built binary file, not a directory or missing artifact
  3. Rebuild the CLI (cargo build) if the binary is absent, then re-run install
  4. Fix packaging/extraction so execute permissions are preserved

Example fix

// before
$ but cli install --source ./dist/but  # not executable
// after
$ chmod +x ./dist/but
$ but cli install --source ./dist/but
Defensive patterns

Strategy: validation

Validate before calling

let md = std::fs::metadata(source)?;
use std::os::unix::fs::PermissionsExt;
if !md.is_file() || md.permissions().mode() & 0o111 == 0 {
    // chmod +x or rebuild the binary before install
}

Prevention

When it happens

Trigger: install_cli_link given a source that is a directory, a non-executable file (missing +x), or a path that exists only as metadata-inaccessible; the ensure! fires when is_file() or the mode check fails.

Common situations: Pointing at a build artifact that wasn't built/failed to build; copying the binary without preserving permissions (losing +x); passing a directory or symlink-to-nothing; files extracted from archives that strip exec bits.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


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

Appendix: source

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

/// Map the error to escalate privileges if permission was denied.
#[cfg(any(target_os = "macos", all(test, unix)))]
fn escalate_privilege_error_if_permission_denied(err: anyhow::Error) -> InstallError {
    match err.downcast_ref::<std::io::Error>() {
        Some(io_err) if io_err.kind() == std::io::ErrorKind::PermissionDenied => {
            InstallError::InstallationRequiresElevatedPrivileges(err)
        }
        _ => InstallError::Other(err),
    }
}

#[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(())

View on GitHub (pinned to 58e5313667)