[BUG] SSH connection fails with "no route to host" (EHOSTUNREACH) despite identical ssh command working outside the app
Bug Summary
Adding an SSH connection in Claude Code Desktop to a specific LAN host fails with "SSH host unreachable (no route to host)", even though the exact same ssh command (same binary, same host, same port, same key) succeeds reliably every time when run manually in Terminal on the same machine, at effectively the same moment.
Captured via a live fs_usage syscall trace: the app's own spawned ssh subprocess performs a real DNS resolution (succeeds, with valid answer data from mDNSResponder), then issues a real connect() syscall that fails in ~150 microseconds with errno 65 (EHOSTUNREACH) — a genuine synchronous kernel-level rejection, not a timeout, and not accompanied by any Sandbox/TCC "deny" log entry.
Environment
- Claude Code Desktop (macOS), versions seen under
~/Library/Application Support/Claude/claude-code: 2.1.258 / 2.1.260 - macOS Darwin 25.6.0
- Target: an SSH server on a LAN host (
myhost-a.example.com→ private IP), resolved via a local DNS override (not public DNS) — same resolution mechanism used successfully by a second, working SSH connection in the same app instance - SSH client actually invoked by the app (per syscall trace): Homebrew-installed OpenSSH at
/opt/homebrew/Cellar/openssh/10.5p1/bin/ssh
Steps to Reproduce
- Add an SSH connection in Claude Code Desktop pointing to a LAN host (hostname or raw IP), correct port, correct key.
- Click "Test connection" (or trigger any action that opens a connection, e.g. sending a message).
- Observe:
SSH host unreachable (no route to host) - From a Terminal on the same Mac, at the same time:
ssh -p <port> user@hostconnects successfully every time, using the identical OpenSSH binary the app itself invokes.
What we ruled out (all directly tested)
- Wrong port — confirmed correct port in the UI field; also tested against a second port mapped on the server (both fail identically).
- DNS resolution — confirmed correct resolution via the same resolver path in Terminal, and via syscall trace showing the app's own
sshsubprocess completing a real mDNSResponder round-trip with valid answer payloads. - ARP cache staleness — confirmed ARP entry present/warm at time of failure; still fails.
- Raw IP instead of hostname — same failure, ruling out hostname-specific resolution issues.
- macOS "Local Network" privacy permission — confirmed already granted in System Settings; also tried toggling off/on + relaunch; no change.
- Third-party firewall / MDM — none installed (no Little Snitch/LuLu/similar found;
profiles statusconfirms not MDM-enrolled). - App Sandbox / TCC network-outbound denial — checked unified system log (
log show) around failure timestamps; zero Sandbox or TCC deny events logged for the process. - Routing table conflicts (e.g. with Tailscale) — routing table shows a single unambiguous route to the target subnet via the physical LAN interface; no competing/overlapping route via any VPN/tunnel interface.
- SSH client binary differences — both Apple's stock
/usr/bin/sshand the Homebrew-installed OpenSSH connect successfully when run manually; trace confirms the app invokes the Homebrew binary directly, same as Terminal. AllowTcpForwarding(tcp_forwarding) server setting — enabled; no change (also not expected to matter, since this fails before SSH protocol negotiation even begins).- Auth method (key vs. password) — not relevant; failure occurs before authentication is ever negotiated (this is a pre-TCP-handshake kernel error).
- App-level caching — fully quit/relaunched the app, and deleted/recreated the connection entry from scratch; same failure.
- Stale permission/policy cache — full system reboot; same failure persists.
Key evidence: syscall trace
Captured via sudo fs_usage -w -f network,exec while triggering the connection from the app. Relevant excerpt from the spawned ssh subprocess (paths/hostnames redacted):
socket F=4 <AF_UNIX, SOCK_STREAM>
connect F=4 /private/var/run/mDNSResponder
sendto/recvfrom ... # real DNS query/response round trip (~6.6ms), valid answer payloads
open /Library/Preferences/com.apple.networkd.plist # read twice, immediately before connect()
socket F=5 <AF_INET, SOCK_STREAM>
connect F=5 [ 65] # fails in ~150 microseconds; 65 = EHOSTUNREACH
close F=5
write F=2 B=0x46 # ~70 bytes to stderr: "ssh: connect to host <host> port <port>: No route to host"
exit [255]
DNS resolution completes successfully, followed by a synchronous kernel-level rejection of connect() in well under a millisecond — far too fast to be a timeout — with no Sandbox/TCC log activity at all. The same destination is reachable via the same physical network path, from the same machine, via a manually-run ssh process, at effectively the same moment.
App's own log output (~/Library/Logs/Claude/ssh.log / main.log)
[info] LocalSessions.testSSHConnection: host=<user>@<host> (probe)
[info] [OpenSSHConnection] starting master for <user>@<host>
[info] [OpenSSHConnection] master stderr: ssh: connect to host <host> port <port>: No route to host
[warn] [OpenSSHConnection] master for <user>@<host> exited before ready; stderr tail: ssh: connect to host <host> port <port>: No route to host
[error] [RemoteServerController] Connection failed (262ms, trigger: send_message, attempt 1): SSH host unreachable (no route to host)
Note: the specific error text is not always identical between attempts — some failures report Network is unreachable (ENETUNREACH) instead of No route to host (EHOSTUNREACH), notably around sleep/wake events, suggesting some non-determinism in the underlying condition.
Expected behavior
The SSH connection should succeed, matching the manually-run ssh command with identical parameters from the same machine.
Additional notes
- A separate, working SSH connection to a different LAN host (
myhost-b.example.com, resolved via the identical private-DNS mechanism) succeeds without any special configuration in the same app instance — this is specific to something about this particular destination/connection, not a global SSH feature failure. We were unable to identify what differs, since every checkable network-layer condition (routing, ARP, DNS, permissions) is identical between the working and failing hosts. - We suspect this may relate to macOS's per-process network policy routing (NECP — Network Extension Control Policy), given
com.apple.networkd.plistis read immediately before the failingconnect(), but couldn't confirm or resolve this without deeper kernel tooling (dtrace/dtrussagainst system binaries requires disabling SIP, which we did not want to do). - Possibly related to #26809 (SSH Port directive from
~/.ssh/configignored), though that issue was about the Port field being ignored — we confirmed the Port field itself is correctly used in this case; the failure happens at a lower network layer than port selection.