{"record":{"id":"73e0c9adc85d535b","repo":"gitbutlerapp/gitbutler","slug":"cli-destination-must-be-absolute","errorCode":null,"errorMessage":"CLI destination must be absolute","messagePattern":"CLI destination must be absolute","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but-action/src/cli.rs","lineNumber":70,"sourceCode":"        Err(InstallError::InstallationRequiresElevatedPrivileges(err)) => match mode {\n            InstallMode::AllowPrivilegeElevation => {\n                install_cli_link_escalated(cli_path, destination, symlink_policy)\n            }\n            InstallMode::CurrentUserOnly => Err(err)\n                .context(\"Privilege escalation required but not allowed under user install mode\"),\n        },\n        Err(InstallError::Other(err)) => Err(err),\n    }\n}\n\n/// Installs the CLI link with escalated privileges.\n#[cfg(any(target_os = \"macos\", all(test, unix)))]\nfn install_cli_link_escalated(\n    cli_path: &std::path::Path,\n    destination: &std::path::Path,\n    symlink_policy: ExistingSymlinkPolicy,\n) -> anyhow::Result<()> {\n    anyhow::ensure!(\n        destination.is_absolute(),\n        \"CLI destination must be absolute\"\n    );\n    let directory = destination\n        .parent()\n        .context(\"CLI destination has no parent\")?;\n    // osascript accepts text arguments, so reject non-UTF-8 instead of changing paths.\n    let source = cli_path.to_str().context(\"CLI path is not valid UTF-8\")?;\n    let target = destination\n        .to_str()\n        .context(\"CLI destination is not valid UTF-8\")?;\n    let directory = directory\n        .to_str()\n        .context(\"CLI destination directory is not valid UTF-8\")?;\n    // The script's destination check and `ln` are not atomic: if a directory appears at the\n    // destination between them, `ln` can create a link inside it. Verification below rejects that\n    // result, but the stray link remains. Avoiding this race would require an elevated helper\n    // calling symlink(2) directly instead of `ln`. That seems a bit overkill for now.","sourceCodeStart":52,"sourceCodeEnd":88,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/58e5313667b857ef39a730e380af31816a7b1768/crates/but-action/src/cli.rs#L52-L88","documentation":"install_cli_link_escalated (macOS) creates a symlink to the CLI binary, possibly via an escalated privilege helper. Before doing anything it ensures the destination path is absolute; a relative destination cannot be safely resolved by the elevated helper, so it aborts with 'CLI destination must be absolute'.","triggerScenarios":"Calling the CLI install v2 flow with a destination path that is relative (e.g. 'bin/but' instead of '/usr/local/bin/but'), reaching the escalated link installer.","commonSituations":"Passing a user-supplied destination from config or a shell variable without canonicalizing it; running under a helper process whose CWD differs from the caller's.","solutions":["Canonicalize the destination (std::fs::canonicalize or join onto '/') before calling the install API","Pass a fully-qualified path like /usr/local/bin/but","If building custom install tooling, resolve relative paths against the intended base directory first"],"exampleFix":"// before\ninstall_cli_link_escalated(&cli, Path::new(\"bin/but\"), policy)\n// after\nlet dest = std::fs::canonicalize(\"bin/but\").unwrap_or_else(|_| base.join(\"bin/but\"));\ninstall_cli_link_escalated(&cli, &dest, policy)","handlingStrategy":"validation","validationCode":"if !destination.is_absolute() {\n    return Err(anyhow!(\"destination must be absolute\"));\n}\n// or: let destination = std::fs::canonicalize(destination_parent)?.join(file_name);","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always canonicalize user-supplied install paths","Never rely on CWD when invoking escalated helpers","Default to known absolute prefixes (/usr/local/bin)","Add is_absolute() asserts in install scripts"],"tags":["cli","install","path","validation"],"backgroundTag":"invalid-argument-value","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"}