Skip to content
shellmap

Check an SSH connection without running anything

Verify that an SSH host is reachable, key auth works, and the server is responsive — without actually opening a shell or running a payload command. The canonical "is the box up + is my key configured" health-check used in deploy scripts and CI preflight.

How to check an ssh connection without running anything in each shell

Bashunix
ssh -T -q -o BatchMode=yes -o ConnectTimeout=5 user@host exit

Canonical CI/deploy preflight. `-T` disables tty allocation; `-q` quiets the banner; `BatchMode=yes` disables ALL prompts (password + passphrase + hostkey-accept) — passes only if key auth succeeds; `ConnectTimeout=5` bounds the TCP handshake; the literal command `exit` does nothing useful but is required because OpenSSH won't connect "with no command" silently (it would open a shell, which we want to skip). Exit code: `0` = SSH connected + authenticated + `exit` ran fine; `255` = ssh itself failed (DNS, TCP refused, auth failed, timeout). Use `0 = healthy, anything else = unhealthy` in deploy preflight: `ssh -T -q -o BatchMode=yes -o ConnectTimeout=5 prod-bastion exit || { echo "bastion not reachable"; exit 1; }`. CAVEAT: this verifies KEY AUTH, not the remote service — if you need port 22 reachability before key setup, use the netcat form (see notes).

Zshunix
ssh -T -q -o BatchMode=yes -o ConnectTimeout=5 user@host exit
Fishunix
ssh -T -q -o BatchMode=yes -o ConnectTimeout=5 user@host exit

No fish-side wrinkle — ssh flags parse identically. To wrap as a fish function for repeat use: `function ssh-check; ssh -T -q -o BatchMode=yes -o ConnectTimeout=5 $argv exit; end` in `~/.config/fish/functions/ssh-check.fish`. Then `ssh-check prod-bastion; and echo OK; or echo DOWN`. fish handles `$argv` cleanly when callers pass multiple args like `ssh-check user@host -p 2222`.

PowerShellwindows
Test-NetConnection -ComputerName host -Port 22 -InformationLevel Quiet

pwsh-native form. `Test-NetConnection -Port 22` does a TCP probe to port 22 — checks REACHABILITY only, not SSH auth. Returns `$true`/`$false` with `-InformationLevel Quiet`. For full SSH-auth check, pwsh ships OpenSSH so the ssh-flag form works identically: `ssh -T -q -o BatchMode=yes -o ConnectTimeout=5 user@host exit; $LASTEXITCODE`. Combine both for two-phase preflight: `if (Test-NetConnection host -Port 22 -InformationLevel Quiet) { ssh -T -q -o BatchMode=yes -o ConnectTimeout=5 user@host exit; if ($LASTEXITCODE -eq 0) { Write-Host "OK" } else { Write-Host "Port 22 open but SSH auth failed" } } else { Write-Host "Port 22 unreachable" }`. The legacy WinRM-side equivalent: `Test-WSMan -ComputerName host` (probes the Windows-Remote-Management endpoint, not SSH).

cmd.exewindows
ssh -T -q -o BatchMode=yes -o ConnectTimeout=5 user@host exit

Same OpenSSH binary. Exit code in cmd: `%ERRORLEVEL%`. CMD does not have a `Test-NetConnection` analogue; the closest is `powershell -Command "Test-NetConnection -ComputerName host -Port 22 -InformationLevel Quiet"`. Legacy alternative without ssh: `telnet host 22` — opens TCP and prints the SSH banner (e.g. `SSH-2.0-OpenSSH_8.9p1`). Exit telnet with Ctrl+]+`quit`. CAVEAT: `telnet` is NOT installed by default on modern Windows; install via `dism /online /Enable-Feature /FeatureName:TelnetClient`. The PuTTY-suite alternative: `plink -batch -ssh user@host exit` — `-batch` disables prompts, `exit` is the no-op remote command.

Equivalents listed for Bash, Zsh, Fish, PowerShell, cmd.exe.

Gotchas & notes

  • **Exit-code contract: rc 0 = full success, rc 255 = ssh failed.** The `ssh -T -q -o BatchMode=yes -o ConnectTimeout=5 user@host exit` invocation has a clean exit-code contract: `0` means the TCP connection succeeded, key auth succeeded, AND the remote `exit` command ran cleanly. `255` means ssh ITSELF failed — could be DNS resolution, TCP refused, ConnectTimeout, auth refused, hostkey verification failed. Distinguish these in CI by capturing stderr: `ssh -T -q -o BatchMode=yes -o ConnectTimeout=5 user@host exit 2>err.log; rc=$?; case $rc in 0) echo OK ;; 255) cat err.log; echo "ssh layer failed" ;; *) echo "remote exit $rc" ;; esac`. Common stderr messages: `Connection refused` (no sshd listening), `Connection timed out` (firewall dropping), `Permission denied (publickey)` (auth failed), `Host key verification failed` (server hostkey changed).
  • **`nc -z` for port-reachability-only check (no SSH protocol involvement).** When you don't care about SSH auth — just "is port 22 open" — `nc -z -w 5 host 22; echo $?` (`-z` = scan, no data sent; `-w 5` = 5s timeout). rc 0 = open, non-zero = closed/filtered/timed out. Faster than ssh because no protocol negotiation. CAVEAT: `nc` (netcat) has multiple incompatible implementations — `nc.openbsd` (Debian default), `nc.traditional` (older), `ncat` (Nmap project). `-z` works on most; `nmap -p 22 host` is the most portable scan ("STATE: open / closed / filtered"). Use `nc -z` for "is it up before I try ssh", use the full ssh-T check for "is my entire deploy path healthy". `bash` has a built-in TCP probe that needs no extra binary: `timeout 5 bash -c "echo > /dev/tcp/host/22" 2>/dev/null && echo open` — opens TCP via the `/dev/tcp/<host>/<port>` magic path. Useful in minimal containers without nc installed.
  • **Hostkey acceptance and `BatchMode=yes` interaction.** First-time ssh to a new host prints `The authenticity of host 'host' can't be established. ECDSA key fingerprint is SHA256:...`. With `BatchMode=yes` this prompt becomes a HARD FAILURE — the script fails with rc 255 + stderr `Host key verification failed`. For CI / preflight where the hostkey isn't pre-populated, options: (1) Pre-populate `~/.ssh/known_hosts` via `ssh-keyscan host >> ~/.ssh/known_hosts` (fetches the server's public hostkey out-of-band; vulnerable to MITM if the network you scan from is compromised — pin the key from a known-good source). (2) Use `-o StrictHostKeyChecking=accept-new` (OpenSSH 7.6+) — auto-accept first-time-seen keys, but error if a key has CHANGED (TOFU model). (3) `-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null` — skip ALL checks (insecure; only for throwaway CI to known-isolated networks). The pinned approach is safest for prod.
  • **Distinguishing CONNECTIVITY failures: DNS vs TCP vs SSH-protocol.** Layered diagnosis: (1) DNS: `getent hosts host` (Linux) / `Resolve-DnsName host` (pwsh) / `nslookup host` (universal). Empty result = DNS broken. (2) Ping: `ping -c 1 -W 2 host` (Linux) / `Test-Connection -Count 1 host -TimeoutSeconds 2` (pwsh). Non-zero rc = ICMP blocked or host down — but many cloud envs drop ICMP, so `ping` failing doesn't prove SSH is down. (3) TCP-22: `nc -z -w 5 host 22` or `bash -c "echo > /dev/tcp/host/22"`. Open = sshd is listening. (4) SSH protocol: `ssh -v user@host exit 2>&1 | head -20` (verbose first 20 lines show the SSH handshake — key exchange, ciphers, hostkey verification). The first error line in `-v` output names the failing layer precisely. (5) Auth specifically: `ssh -v user@host 2>&1 | grep -E "Offering|Authentication" ` — shows which key files were tried and what the server replied.
  • **ServerAliveInterval / TCPKeepAlive for "is the EXISTING connection still alive".** Different question from "can I newly connect": "is my open ssh session still healthy". OpenSSH options `ServerAliveInterval 30` + `ServerAliveCountMax 3` (in `~/.ssh/config`) send an encrypted-channel keepalive every 30s; after 3 missed responses (90s total), ssh disconnects with `Timeout, server not responding`. `TCPKeepAlive yes` (default) is a separate, OS-level TCP keepalive — coarser-grained (often 2-hour default before first probe). For tunneled connections behind aggressive NAT timeouts (mobile / hotel WiFi often drop idle TCP at 5–10 min), set `ServerAliveInterval 30` aggressively. For "verify health of an active session from another shell": `ssh -O check -S ~/.ssh/cm-%h-%p-%r user@host` (requires ControlMaster). rc 0 = master alive; non-zero = dead.

Related tasks