gitbutlerapp/gitbutler · warning

Would probably need to run "ln -sf

Error message

Would probably need to run "ln -sf '{}' '{UNIX_LINK_PATH}'"{privilege}

What it means

Returned by the CLI installer when it cannot create the /usr/local/bin/but symlink itself and falls back to instructing the user. It is reached when the earlier direct symlink attempts failed (no permission on /usr/local/bin), the platform is not macOS, or InstallMode::CurrentUserOnly disallows elevation. The message contains the exact ln -sf command to run, optionally suffixed with ' with root permissions'.

Solutions

  1. Run the exact command from the error message: sudo ln -sf '<printed-cli-path>' /usr/local/bin/but
  2. Or install into a directory you own that is on PATH: mkdir -p ~/bin && ln -sf '<cli>' ~/bin/but
  3. On macOS, retry with AllowPrivilegeElevation so the osascript path performs the privileged link
  4. Verify the cli binary exists at the printed path before linking (dev builds: build the but binary first)

Example fix

# before: install returns "Would probably need to run \"ln -sf ...\""
# after: run the printed command
sudo ln -sf '/path/to/but' /usr/local/bin/but
# or user-local alternative:
mkdir -p ~/bin && ln -sf '/path/to/but' ~/bin/but
echo 'export PATH="$HOME/bin:$PATH"' >> ~/.profile
Defensive patterns

Strategy: fallback

Validate before calling

// can we write /usr/local/bin without elevation?
let probe = std::path::Path::new("/usr/local/bin/.probe");
let unprivileged = std::fs::write(probe, b"").is_ok();
let _ = std::fs::remove_file(probe);
if !unprivileged {
    // expect the manual-instruction error; guide the user up front
}

Try / catch

match do_install_cli(mode) {
    Err(e) if e.to_string().contains("Would probably need to run") => {
        // extract the ln -sf command from the message and run/guide it
    }
    r => r,
}

Prevention

When it happens

Trigger: do_install_cli on Linux (any mode), or on macOS with CurrentUserOnly: the non-privileged std::os::unix::fs::symlink into /usr/local/bin fails with permission denied, control falls past the osascript branch, and the else-branch returns this instruction error.

Common situations: Linux systems where /usr/local/bin is root-owned; macOS users choosing 'current user only'; headless or automation contexts where no interactive privilege prompt is possible; dev builds where the but binary does not exist yet (that trips a different message first).

Related errors


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

Appendix: source

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

            // dedicated Code so the frontend can react based on the code
            // rather than matching on an English message.
            Err(
                anyhow!("osascript exited with status 1").context(ErrorContext::new_static(
                    Code::CliInstallCancelled,
                    "CLI install cancelled",
                )),
            )
        } else {
            Err(anyhow!(
                "osascript exited with status {}",
                status
                    .code()
                    .map(|c| c.to_string())
                    .unwrap_or_else(|| "unknown".into())
            ))
        }
    } else {
        Err(anyhow!(
            "Would probably need to run \"ln -sf '{}' '{UNIX_LINK_PATH}'\"{privilege}",
            cli_path.display(),
            privilege = if can_elevate_privileges {
                " with root permissions"
            } else {
                ""
            }
        ))
    }
}

fn ensure_cli_path_exists_prior_to_link(cli_path: &std::path::Path) -> anyhow::Result<()> {
    if cli_path.exists() {
        return Ok(());
    }
    bail!("Run `CARGO_TARGET_DIR=$PWD/target/tauri cargo build -p but` to build the `but` binary")
}

View on GitHub (pinned to caf1f223d3)