fix: clarify remote ssh auth failures

refs #1034
This commit is contained in:
akbash-bot
2026-07-06 15:14:23 +00:00
parent dd67f8591e
commit 1068af4fa2
4 changed files with 92 additions and 1 deletions
+1
View File
@@ -17,6 +17,7 @@
- Bumped the client/server protocol version to 15 for socket API placement mutation event and response compatibility.
### Fixed
- `herdr --remote` now prints clean remote attach failures and SSH authentication guidance instead of Rust Debug-formatted I/O errors when SSH authentication is denied. (#1034)
- `herdr server stop` now waits until both server sockets are unreachable before returning, avoiding an immediate first-start failure when restarting right after replacing the binary.
- Grok Build agent detection now tracks the current Grok Build UI: panes report working while responses, tools, and subagents run, and blocked on permission prompts and question dialogs, instead of falling back to idle mid-turn. (#1017)
- Unix local Herdr clients no longer treat empty bracketed paste as a clipboard-image bridge; `herdr --remote` keeps using it for local-desktop image paste over SSH. (#986)
@@ -69,6 +69,15 @@ Native Windows `herdr --remote` is not part of the Windows beta. From Windows, S
By default, `herdr --remote` runs remote setup and the bridge through a temporary SSH config that includes your SSH config first, then adds fallback keepalive settings and a private per-attach control socket for connection reuse. Existing user keepalive settings win. Set `[remote].manage_ssh_config = false` to use plain `ssh` without Herdr's generated config or control socket.
Remote attach uses your normal OpenSSH authentication. If the target uses a passphrase-protected key in a non-interactive shell, script, CI job, or mobile terminal that cannot show the passphrase prompt, load the key into ssh-agent first:
```bash
ssh-add
herdr --remote workbox
```
For any remote authentication failure, verify plain SSH access first with `ssh workbox`, then run `herdr --remote workbox` again.
By default, remote attach uses the normal restart/stop flow if it needs to replace or restart a running remote server. To opt into experimental live handoff for a supported running remote server, pass `--handoff`:
```bash
+7 -1
View File
@@ -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();
+75
View File
@@ -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'");
}
}