Problem / Background
Interactive-mode connection failures print only the outermost anyhow context, hiding the root cause.
Motivating incident: a user ran bssh -J <jumphost> user@192.168.1.22 against a macOS host that had just rebooted. bssh printed only:
✗ Failed to connect to inureyes@192.168.1.22: Failed to establish jump host connection to 192.168.1.22
Error: Failed to connect to any nodes
The actual failing step (the direct-tcpip channel open from the jump host to the destination, refused because the destination's sshd was not yet accepting connections) was invisible. The same command via OpenSSH surfaced its progress step by step, so the user misattributed the cause. With the full context chain displayed, the root cause would have been obvious on the first run.
Technical Analysis
src/commands/interactive/execution.rs:64 and src/commands/interactive/execution.rs:133 print connection errors with eprintln!("✗ Failed to connect to {}: {}", node.to_string().red(), e). anyhow's {} Display shows only the outermost context message.
- The jump path wraps the real error in multiple nested contexts that are all swallowed:
src/commands/interactive/connection.rs:296-301: "Failed to establish jump host connection to {host}:{port}" (this is the only context the user ever sees)
src/jump/chain.rs:272-277: "Failed to connect to first jump host: ..."
src/jump/chain.rs:304-311: "Failed to connect to jump host ... (hop N)"
src/jump/chain.rs:329-333: "Failed to connect to destination ... through jump host chain"
src/jump/chain/tunnel.rs adds the innermost causes: timeout opening tunnel (lines 62-69), "Failed to open direct-tcpip channel to destination ..." (lines 206-210), SSH handshake failures (lines 240-250), and authentication failures (lines 256-262).
- Depending on which hop fails, the user-visible message is identical, so first-jump auth failures, destination TCP refusals, and destination auth failures are indistinguishable.
- Inconsistency inside the codebase: the parallel exec path already formats errors with the alternate form
format!("{e:#}") at src/executor/parallel.rs:517, which renders the full chain. Interactive mode is the outlier.
Proposed Solution
- In both sites in
src/commands/interactive/execution.rs, render the full chain, either with {e:#} (single line, "outer: mid: inner") or by iterating e.chain() into an indented "Caused by:" list for readability. Prefer extracting a small helper (for example format_connection_error(&anyhow::Error) -> String) so both the PTY path and the multiplex path share it and it can be unit tested.
- Audit other user-facing
eprintln!/println! sites in src/commands/ that print anyhow errors with {} and switch them to the same helper where a wrapped chain exists.
- Keep tracing verbosity (
-vv) as is; this change is about the default non-verbose UX.
Acceptance Criteria
Problem / Background
Interactive-mode connection failures print only the outermost anyhow context, hiding the root cause.
Motivating incident: a user ran
bssh -J <jumphost> user@192.168.1.22against a macOS host that had just rebooted. bssh printed only:The actual failing step (the direct-tcpip channel open from the jump host to the destination, refused because the destination's sshd was not yet accepting connections) was invisible. The same command via OpenSSH surfaced its progress step by step, so the user misattributed the cause. With the full context chain displayed, the root cause would have been obvious on the first run.
Technical Analysis
src/commands/interactive/execution.rs:64andsrc/commands/interactive/execution.rs:133print connection errors witheprintln!("✗ Failed to connect to {}: {}", node.to_string().red(), e). anyhow's{}Display shows only the outermost context message.src/commands/interactive/connection.rs:296-301: "Failed to establish jump host connection to {host}:{port}" (this is the only context the user ever sees)src/jump/chain.rs:272-277: "Failed to connect to first jump host: ..."src/jump/chain.rs:304-311: "Failed to connect to jump host ... (hop N)"src/jump/chain.rs:329-333: "Failed to connect to destination ... through jump host chain"src/jump/chain/tunnel.rsadds the innermost causes: timeout opening tunnel (lines 62-69), "Failed to open direct-tcpip channel to destination ..." (lines 206-210), SSH handshake failures (lines 240-250), and authentication failures (lines 256-262).format!("{e:#}")atsrc/executor/parallel.rs:517, which renders the full chain. Interactive mode is the outlier.Proposed Solution
src/commands/interactive/execution.rs, render the full chain, either with{e:#}(single line, "outer: mid: inner") or by iteratinge.chain()into an indented "Caused by:" list for readability. Prefer extracting a small helper (for exampleformat_connection_error(&anyhow::Error) -> String) so both the PTY path and the multiplex path share it and it can be unit tested.eprintln!/println!sites insrc/commands/that print anyhow errors with{}and switch them to the same helper where a wrapped chain exists.-vv) as is; this change is about the default non-verbose UX.Acceptance Criteria
src/executor/parallel.rs:517) is unchanged.