{"record":{"id":"f37a24700a9d44b6","repo":"gitbutlerapp/gitbutler","slug":"cli-source-must-be-an-executable-file","errorCode":null,"errorMessage":"CLI source '{}' must be an executable file","messagePattern":"CLI source '(.+?)' must be an executable file","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but-action/src/cli.rs","lineNumber":178,"sourceCode":"/// Map the error to escalate privileges if permission was denied.\n#[cfg(any(target_os = \"macos\", all(test, unix)))]\nfn escalate_privilege_error_if_permission_denied(err: anyhow::Error) -> InstallError {\n    match err.downcast_ref::<std::io::Error>() {\n        Some(io_err) if io_err.kind() == std::io::ErrorKind::PermissionDenied => {\n            InstallError::InstallationRequiresElevatedPrivileges(err)\n        }\n        _ => InstallError::Other(err),\n    }\n}\n\n#[cfg(any(target_os = \"macos\", all(test, unix)))]\nfn validate_cli_source(source: &std::path::Path) -> anyhow::Result<()> {\n    use std::os::unix::fs::PermissionsExt;\n\n    anyhow::ensure!(source.is_absolute(), \"CLI source path must be absolute\");\n    let metadata = std::fs::metadata(source)\n        .with_context(|| format!(\"Cannot access CLI executable at '{}'\", source.display()))?;\n    anyhow::ensure!(\n        metadata.is_file() && metadata.permissions().mode() & 0o111 != 0,\n        \"CLI source '{}' must be an executable file\",\n        source.display()\n    );\n    Ok(())\n}\n\n#[cfg(any(target_os = \"macos\", all(test, unix)))]\nfn verify_cli_link(source: &std::path::Path, destination: &std::path::Path) -> anyhow::Result<()> {\n    let target = std::fs::read_link(destination).context(\"Cannot read installed CLI symlink\")?;\n    anyhow::ensure!(\n        target == source,\n        \"Refusing to replace '{}', which points to '{}' instead of '{}'\",\n        destination.display(),\n        target.display(),\n        source.display()\n    );\n    Ok(())","sourceCodeStart":160,"sourceCodeEnd":196,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/58e5313667b857ef39a730e380af31816a7b1768/crates/but-action/src/cli.rs#L160-L196","documentation":"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'.","triggerScenarios":"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.","commonSituations":"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.","solutions":["chmod +x the CLI binary at the source path","Verify the path points to the actual built binary file, not a directory or missing artifact","Rebuild the CLI (cargo build) if the binary is absent, then re-run install","Fix packaging/extraction so execute permissions are preserved"],"exampleFix":"// before\n$ but cli install --source ./dist/but  # not executable\n// after\n$ chmod +x ./dist/but\n$ but cli install --source ./dist/but","handlingStrategy":"validation","validationCode":"let md = std::fs::metadata(source)?;\nuse std::os::unix::fs::PermissionsExt;\nif !md.is_file() || md.permissions().mode() & 0o111 == 0 {\n    // chmod +x or rebuild the binary before install\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Verify the binary exists and is built before install","Preserve exec bits when copying/archiving","chmod +x after extracting from archives","Point installers at the file, never a directory"],"tags":["cli","install","permissions","validation"],"backgroundTag":"permission-denied","analyzedSha":"58e5313667b857ef39a730e380af31816a7b1768","analyzedAt":"2026-09-18T06:50:32.052Z","contentChangedAt":"2026-09-18T06:50:32.052Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}