anthropics / anthropics/claude-code
[BUG] SSH connection fails with "no route to host" (EHOSTUNREACH) despite identical ssh command working outside the app
- Dominant language
- Python
- Stars
- 145k
- Forks
- 23.1k
- PR merge metrics
- PR metrics pending
Description
## 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
1. Add an SSH connection in Claude Code Desktop pointing to a LAN host (hostname or raw IP), correct port, correct key.
2. Click "Test connection" (or trigger any action that opens a connection, e.g. sending a message).
3. Observe: `SSH host unreachable (no route to host)`
4. From a Terminal on the same Mac, at the same time: `ssh -p user@host` connects 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 `ssh` subprocess 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 status` confirms 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/ssh` and 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
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
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 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=@ (probe)
[info] [OpenSSHConnection] starting master for @
[info] [OpenSSHConnection] master stderr: ssh: connect to host port : No route to host
[warn] [OpenSSHConnection] master for @ exited before ready; stderr tail: ssh: connect to host 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.plist` is read immediately before the failing `connect()`, but couldn't confirm or resolve this without deeper kernel tooling (`dtrace`/`dtruss` against system binaries requires disabling SIP, which we did not want to do).
- Possibly related to #26809 (SSH Port directive from `~/.ssh/config` ignored), 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.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with LocalSessions.testSSHConnection and the OpenSSHConnection logs at ~/Library/Logs/Claude/ssh.log and main.log, then reproduce the failure while comparing the app-spawned ssh process with the manual command. Use the reported fs_usage trace and macOS network behavior as the initial evidence; done means the app connects to the affected LAN host reliably with the existing parameters.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- macos
- Domain
- desktop-dev, networking
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 30/100