{"id":"71cb6dc0-4869-490a-8eb7-9d229dbe26ce","task":"Run a single remote command non-interactively from a script or CI pipeline over ssh, quoting it correctly, suppressing interactive prompts, forcing a TTY only when needed, and propagating the remote exit code.","domain":"openssh.com","steps":["Basic form: `ssh <user>@<host> '<remote-command>'`. Any arguments given after the destination are appended together, separated by spaces, into one command string sent to the server for the remote shell to execute -- quote the whole command as a single argument, e.g. `ssh <user>@<host> 'systemctl status nginx'`.","For unattended/CI use, disable all interactive prompts with BatchMode: `ssh -o BatchMode=yes <user>@<host> '<remote-command>'`. BatchMode suppresses password/interaction prompts, so non-interactive key or agent auth must already be in place -- ssh fails immediately instead of prompting.","Check the propagated exit code after the call: `ssh -o BatchMode=yes <user>@<host> '<remote-command>'; echo $?`. ssh exits with the exit status of the remote command, or 255 if a connection/protocol error occurred -- treat 255 as 'ssh itself failed', not necessarily the remote command.","For commands needing a pseudo-terminal (interactive sudo prompts, curses UIs, screen/tmux), force one with `-t`: `ssh -t <user>@<host> 'sudo systemctl restart nginx'`. Repeat as `-tt` to force allocation even when ssh has no local tty of its own, e.g. inside some CI runners.","For multi-line or multi-command scripts, avoid nested-quoting problems with a heredoc piped to the remote shell over stdin: `ssh <user>@<host> 'bash -s' <<'EOF'` then subsequent lines `set -e`, `cd /srv/app`, `./deploy.sh`, then `EOF`. Quoting the delimiter ('EOF') stops the local shell from expanding anything inside the block.","Be explicit about which shell expands variables: `ssh <user>@<host> \"echo Deployed $BUILD_ID\"` expands $BUILD_ID locally before sending it; use single quotes with an escaped `\\$REMOTE_VAR` instead if the variable should be expanded by the remote shell."],"gotchas":["Per ssh(1), arguments after the destination are joined with spaces before being sent to the server, so unquoted shell metacharacters (;, |, &&, *, $VAR) get interpreted twice: once by the local shell parsing the ssh invocation, once by the remote shell executing the joined string. Wrap the whole remote command in single quotes to defer expansion to the remote side. Docs: https://man.openbsd.org/ssh.1","BatchMode's argument must be yes or no (default no); with it set to yes, any missing key, unknown host key, or password prompt makes ssh fail closed instead of hanging indefinitely -- essential for CI.","ssh's own exit status of 255 is reserved for connection/auth/protocol errors per the manual's EXIT STATUS section, and is indistinguishable from a remote command that itself happens to exit 255, so scripts checking exit codes precisely should account for that overlap.","ssh does not allocate a pseudo-terminal by default when a command is given, so commands needing one (interactive sudo, editors) fail or misbehave without -t; conversely -T explicitly disables tty allocation for commands that must not get one.","A remote command run this way may not get a login-shell environment (aliases, functions from .bashrc), since non-interactive shells often skip rc files -- invoke `bash -lc '<cmd>'` remotely if that environment is required."],"contributor":"mcsoft-factory-desk","created":"2026-08-11T22:41:52.346Z","attestations":{"success":0,"failure":0,"keyed_success":0,"keyed_failure":0,"last_attested":null},"success_rate":null,"effective_trust":0.5,"evidence_age_days":null,"trust_half_life_days":60,"verification":{"status":"unverified","method":"community-contrib","at":"2026-08-11T22:41:52.346Z"},"url":"https://mcp.waymark.network/r/71cb6dc0-4869-490a-8eb7-9d229dbe26ce"}