| name | compuse-ssh |
| description | Use when running commands over SSH or driving a remote host - checking or killing remote processes, quoting an inline remote command, backgrounding a remote command, running remote Windows PowerShell, an SSH login asking for a password, a changed or unknown host key ("host key verification failed", "remote host identification has changed"), setting up SSH key-based auth, a remote command failing with exit code 255, or a slow, stalled, or failed remote download or transfer. |
computer-use-ssh
Quick reference
| Situation | Rule |
|---|
| Check/kill a remote process by name | ssh host '... pgrep/pkill -f X ...' makes the remote shell's OWN argv contain X, so it self-matches (false positive; pkill kills your ssh shell mid-command). Prefer signals that can't match an argv: systemctl is-active <unit>, a cgroup dir, a listening port (ss -ltnH | grep :PORT), pidfile + kill -0. |
| Must use pgrep/pkill remotely | Bracket the first char ([x]pattern) AND keep that keyword out of any echo label in the same command (the label re-introduces the literal). To be sure, exclude the current shell: ... | grep -vw "$$". |
| Quoting an inline remote command | Nested quotes and awk '{print $1}' / cut -d" " inside ssh '...' get eaten by the outer shell (the $1 or the " is consumed). Prefer a detached on-host script (scp it, then run it) over a long inline one-liner. |
| Backgrounding a remote command | ssh host '... &' drops the session (exit 255) when the backgrounded process holds the tty/pipe. Use setsid CMD </dev/null >/dev/null 2>&1 & or a detached on-host script. |
| Long remote reload/restart over SSH | A long step (services_*_configure(), nginx -t && reload, apt upgrade) can outlive the client ConnectTimeout; the remote command keeps running ORPHANED and SUCCEEDS. Never infer failure from a dropped SSH connection - re-query the real state afterward (systemctl is-active, the listening port, DNS, an HTTP probe). |
| Slow/large remote download (GB image, model, tarball) | Detach it so the client ConnectTimeout can't kill it mid-transfer: scp a script that does the download, launch it setsid bash script >LOG 2>&1 </dev/null &, then poll LOG + the file SIZE from separate short ssh calls. CONFIRM it moves: measure the byte delta over ~10s - a plausible rate (MB/s for a GB payload), NOT 0. Resume across retries with curl -C -. Do NOT put a pkill -f <name> in the SAME command that launches <name>: the launcher's own argv contains <name>, so pkill kills the launcher before setsid runs. |
| ANY remote command fails - ROOT-CAUSE it, never wave it off | Exit 255 = ssh ITSELF failed (connect, auth, host key, dropped session) - the remote command may never have run; any OTHER code IS the remote command's own exit. Get evidence before acting: read stderr, rerun a no-op probe with ssh -v host 'echo ok' (shows the DNS/connect/auth/host-key stage), then read the REMOTE state and logs (systemctl status, journalctl -u <unit>, the app's own log). "Flaky network" is a hypothesis to TEST with that probe, never a diagnosis to assume - a blind retry of a mutating command (restart, install, migration step) double-fires it. |
| A remote download "stalls"/"fails" - ROOT-CAUSE it, do not assume "slow network" | 0 bytes + curl: (22) ... 404 is a WRONG URL/asset name, not flakiness - retrying or detaching a wrong URL wastes hours. Read the actual curl exit code + HTTP status FIRST; if unsure of the asset, fetch the tool's own installer/source to get the exact name (e.g. ollama publishes ollama-linux-amd64.tar.zst, not .tgz). Only call it "slow" once you have SEEN a plausible partial transfer. |
| Remote Windows PowerShell | NEVER inline ssh host 'powershell -Command "...|..."' - the pipe and quotes pass through bash -> ssh -> cmd.exe and cmd eats them. ALWAYS write a .ps1 and run it with -File. |
| Host wants a password | NEVER ask for, type, or accept an SSH password - it leaks into the transcript, shell history, and logs. Use key auth: ssh -i <keypath>. If there is no key, STOP and propose the user set one up (see below). |
| Connecting on your OWN/trusted subnet | Hosts get reimaged, so keys change. Accept new AND changed: ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ..., scoped in ~/.ssh/config to the subnet ranges ONLY. Untrusted hosts: accept-new. |
Why inline remote commands break
A command in ssh host '...' is parsed by the LOCAL shell, then the REMOTE shell, then (for Windows) cmd.exe, then PowerShell. Each layer strips a level of quoting and can self-match patterns. A file moved with scp and run by path crosses none of those layers, which is why a detached on-host script beats a clever one-liner for anything non-trivial.
An SSH session is a flaky external resource: it can drop, hang, or time out mid-command. Never infer failure from a dropped connection - retry under a timeout and re-query the real state. For the self-healing patterns (retry+backoff, timeouts, graceful degradation), see bitranox:coding-resilience.
Authentication and host keys
Setting it up (ask the user's OS, then walk them through it)
When a host needs auth, first ask which operating system the user is on, then guide them. Afterwards
they hand you only the private-key PATH and you log in with ssh -i <keypath> user@host.
Linux / macOS:
- Generate a key:
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 (a passphrase + ssh-agent is the
safer choice; a no-passphrase key works but is a credential at rest, so store it well).
- Install the public half on the host:
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@host (or append
id_ed25519.pub to the host's ~/.ssh/authorized_keys by hand).
- Private key stays at
~/.ssh/id_ed25519 (chmod 600); the user gives you that path.
Windows:
- Ensure the OpenSSH client exists (Windows 10/11 usually ship it; check with
ssh -V). If missing,
install it - winget: winget install Microsoft.OpenSSH.Beta; or the built-in optional feature in
an elevated PowerShell: Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0.
- Generate a key:
ssh-keygen -t ed25519 -f $env:USERPROFILE\.ssh\id_ed25519.
- There is no
ssh-copy-id on Windows - install the public key on the host by hand, e.g.
type $env:USERPROFILE\.ssh\id_ed25519.pub | ssh user@host "cat >> ~/.ssh/authorized_keys".
- Private key is at
C:\Users\<you>\.ssh\id_ed25519; the user gives you that path.
Setting up the SSH server on the host (per OS, if it has none)
If the host has no SSH server yet, walk the user through enabling one (ask the OS):
Linux (Debian/Ubuntu): sudo apt install openssh-server && sudo systemctl enable --now ssh
(RHEL/Fedora: sudo dnf install -y openssh-server && sudo systemctl enable --now sshd).
macOS: built in - enable Remote Login: sudo systemsetup -setremotelogin on
(or System Settings > General > Sharing > Remote Login).
Windows (elevated PowerShell):
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
Start-Service sshd
Set-Service -Name sshd -StartupType Automatic
# open the firewall if the rule is missing:
New-NetFirewallRule -Name sshd -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22
The default ssh shell on Windows is cmd.exe; to make it PowerShell set the DefaultShell value under HKLM:\SOFTWARE\OpenSSH.
Hook / script
block-pgrep-self-match (PreToolUse on Bash) catches the echo-label pgrep self-match, including ssh ... 'pgrep ...'. For remote PowerShell, a runps.sh-style wrapper that syntax-checks the .ps1 with pwsh locally, then does the scp + -File run, avoids the cmd-quoting trap.