aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorakbash <akbash@herdr.dev>2026-07-06 18:21:31 +0300
committerGitHub <noreply@github.com>2026-07-06 18:21:31 +0300
commitd9230f517a88be0eb88c9af39f7a4212644c9a3d (patch)
tree745973b12bad8557ef4c3e06fe6ef5d07fc8fc7e /src
parente98c49658caf054f2cfdc0f52831f29d59ed6fba (diff)
fix: clarify remote ssh auth failures (#1084)
refs #1034 Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com>
Diffstat (limited to 'src')
-rw-r--r--src/main.rs8
-rw-r--r--src/remote.rs75
2 files changed, 82 insertions, 1 deletions
diff --git a/src/main.rs b/src/main.rs
index 3b4bed1c..deb332b9 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -661,7 +661,13 @@ fn main() -> io::Result<()> {
}
if let Some(remote_launch) = remote_launch {
- return remote::run_remote(remote_launch);
+ let remote_target = remote_launch.target.clone();
+ if let Err(err) = remote::run_remote(remote_launch) {
+ eprintln!("error: {err}");
+ remote::print_remote_error_hint(&err, &remote_target);
+ std::process::exit(1);
+ }
+ return Ok(());
}
let loaded_config = config::Config::load();
diff --git a/src/remote.rs b/src/remote.rs
index edfb9269..ce4ca91b 100644
--- a/src/remote.rs
+++ b/src/remote.rs
@@ -146,3 +146,78 @@ pub(crate) fn run_remote_client_bridge() -> std::io::Result<()> {
"remote client bridge is not supported on Windows yet",
))
}
+
+pub(crate) fn print_remote_error_hint(err: &std::io::Error, target: &str) {
+ if is_remote_auth_error(err) {
+ eprintln!(
+ "hint: verify SSH access first with `{}`.",
+ ssh_check_command(target)
+ );
+ eprintln!(
+ "hint: if your SSH key has a passphrase, load it into ssh-agent with `ssh-add` before running `herdr --remote`."
+ );
+ }
+}
+
+fn is_remote_auth_error(err: &std::io::Error) -> bool {
+ let message = err.to_string();
+ message.contains("Permission denied")
+ && (message.contains("(publickey")
+ || message.contains("(keyboard-interactive")
+ || message.contains("(password"))
+}
+
+fn ssh_check_command(target: &str) -> String {
+ format!("ssh {}", shell_quote(target))
+}
+
+fn shell_quote(value: &str) -> String {
+ if !value.is_empty()
+ && value.chars().all(|ch| {
+ ch.is_ascii_alphanumeric()
+ || matches!(
+ ch,
+ '@' | '%' | '_' | '+' | '=' | ':' | ',' | '.' | '/' | '-'
+ )
+ })
+ {
+ return value.to_string();
+ }
+
+ format!("'{}'", value.replace('\'', "'\\''"))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn remote_auth_error_matches_ssh_auth_denied() {
+ let err = std::io::Error::other(
+ "remote platform detection failed: user@host: Permission denied (publickey).",
+ );
+
+ assert!(is_remote_auth_error(&err));
+ }
+
+ #[test]
+ fn remote_auth_error_matches_keyboard_interactive_denied() {
+ let err = std::io::Error::other(
+ "remote server status failed: user@host: Permission denied (keyboard-interactive).",
+ );
+
+ assert!(is_remote_auth_error(&err));
+ }
+
+ #[test]
+ fn remote_auth_error_ignores_non_auth_errors() {
+ let err = std::io::Error::other("remote platform detection failed: unsupported platform");
+
+ assert!(!is_remote_auth_error(&err));
+ }
+
+ #[test]
+ fn ssh_check_command_quotes_remote_target() {
+ assert_eq!(ssh_check_command("host name"), "ssh 'host name'");
+ }
+}