| Tested on | RHEL 10.2 (Coughlan) — vm2.lab.example (192.168.56.220) as SSH client; vm1.lab.example (192.168.56.116) as remote host |
|---|---|
| Package | openssh-clients 9.9p1-25.el10_2 and openssh-server 9.9p1-25.el10_2 |
| Applies to | RHEL, Rocky Linux, AlmaLinux, Fedora, Ubuntu, Debian, and other Linux systems with OpenSSH |
| Privilege | SSH login to the remote host with permission to run the commands you chain together |
| Scope | One-shot remote command chains with ;, &&, ||, subshell grouping, bash -s, here documents, and brief background & notes. Does not cover long-running jobs that must survive disconnect or SSH multiplexing. |
| Related guides | ssh command Long-running commands over SSH SSH multiplexing Copy folder over SSH Test SSH connection |
A single ssh invocation can run several remote steps in one session when you chain them with shell operators inside one quoted command string.
The examples below were run from vm2.lab.example using the lab-server host alias (pointing at vm1.lab.example). Replace that name with your own user@host or SSH config alias.
Quick answer
| Goal | Pattern |
|---|---|
| Always run the next command | ssh lab-server 'cmd1; cmd2' |
| Run next only on success | ssh lab-server 'cmd1 && cmd2' |
| Run fallback on failure | ssh lab-server 'cmd1 || cmd2' |
| Run an existing local script remotely | ssh lab-server 'bash -s' < script.sh |
| Run several inline lines | ssh lab-server <<'EOF' … EOF |
Wrap the entire remote command in single quotes on the client so your local shell does not interpret ;, &&, $variables, or redirects before SSH runs.
How SSH executes multiple commands
When you supply a command after the hostname:
ssh lab-server 'echo hello; hostname'SSH sends that command to the remote host, where the remote user's shell interprets operators such as ;, &&, ||, pipes, and redirects. OpenSSH documents this as executing the specified command on the remote host instead of starting an interactive login session.
All chaining in this article happens on the remote side inside that one command string. Two details matter for later sections:
cmd1 & cmd2 &backgrounds jobs in the same remote shell. It does not launch separate SSH connections, and the client may still wait until background file descriptors on the SSH channel close. For work that must continue after you disconnect, use the patterns in long-running commands over SSH.- Exit status: with semicolon-separated commands, the SSH client exit code reflects the last command in the chain, not an earlier failure in the middle.
Use single quotes around the remote command when you need remote shell variables expanded on the server:
ssh lab-server 'echo "$HOME"; hostname'Sample output:
/root
vm1.lab.exampleSingle quotes stop your local shell from substituting $HOME; the remote shell expands it.
Run every command with semicolon (;)
Use semicolon when each step should run even if a previous step failed:
ssh lab-server 'echo first; echo second; false; echo third'Sample output:
first
second
thirdThe failed false in the middle did not stop echo third. Check whether the chain failed overall:
ssh lab-server 'echo first; false; echo third'; echo "client exit: $?"Sample output:
first
third
client exit: 0The client reports exit 0 because the last command (echo third) succeeded, even though false ran in between.
Stop on failure with &&
Use && when later steps depend on earlier ones succeeding:
ssh lab-server 'mkdir -p /tmp/ssh-multi-lab && cd /tmp/ssh-multi-lab && printf "ok\n" > demo.txt && cat demo.txt'Sample output:
okIf any step returns non-zero, the rest of the chain is skipped:
ssh lab-server 'false && echo should-not-run'Sample output:
No output appears because false failed first. Clean up the lab directory when you are done:
ssh lab-server 'rm -rf /tmp/ssh-multi-lab'Run a fallback with ||
Use || when the second command should run only if the first fails:
ssh lab-server 'test -f /etc/shadow || echo missing-file'Sample output:
On a normal system /etc/shadow exists, so the echo does not run. A failing test triggers the fallback:
ssh lab-server 'test -f /no/such/file || echo fallback-ran'Sample output:
fallback-ranGroup commands and control cd
cd in a remote chain affects every later command in the same shell unless you isolate it.
Without grouping, both pwd lines run under /tmp/ssh-multi-lab after the first cd:
ssh lab-server 'mkdir -p /tmp/ssh-multi-lab && cd /tmp/ssh-multi-lab && pwd; cd /var && pwd'Sample output:
/tmp/ssh-multi-lab
/varParentheses run the grouped commands in a subshell, so cd does not leak out:
ssh lab-server '( cd /tmp/ssh-multi-lab; pwd ); pwd'Sample output:
/tmp/ssh-multi-lab
/rootBraces { cd …; cmd; } group commands in the current shell, so a cd inside the group persists afterward — the opposite of parentheses. Prefer ( … ) when you only want a temporary working directory for one step.
Remove the lab directory:
ssh lab-server 'rm -rf /tmp/ssh-multi-lab'Run a local script with bash -s
When the commands already live in a file on your client, redirect that file into bash -s on the remote host:
ssh lab-server 'bash -s' < local.shbash -s tells Bash on the remote host to read the script from standard input. The file does not have to exist on the server disk.
Create local.sh on your workstation with echo from-script and uname -r, then run the redirect above. Sample output from the lab:
from-script
6.12.0-211.47.1.el10_2.x86_64Use this pattern when you maintain reusable automation locally and want one SSH invocation to execute it remotely.
Run multiple lines with a here document
When the commands are inline in your shell script or terminal — not saved in a separate file — attach a here document to ssh:
ssh lab-server <<'REMOTE'
echo from-heredoc
pwd
REMOTESample output (login banner lines omitted):
from-heredoc
/rootQuote the delimiter (<<'REMOTE') when you do not want the client shell to expand $variables before the text reaches SSH. Add -T when you want to disable pseudo-tty allocation for cleaner non-interactive output.
Summary of the two multi-line patterns:
| You have | Use |
|---|---|
| An existing script file on the client | ssh host 'bash -s' < script.sh |
| Several inline lines in the current shell | ssh host <<'EOF' … EOF |
Background commands with &
Single & sends each command to the background inside the same remote shell:
time ssh lab-server 'echo a & sleep 2 & wait'Sample output:
a
real 0m2.651s
user 0m0.011s
sys 0m0.020sThe client waited until wait collected the background jobs. Without wait, behavior depends on how the jobs connect to the SSH channel — the client may still block. Treat & as a shell background operator, not as a way to fire off independent remote sessions.
For jobs that must keep running after SSH exits, use nohup, setsid, or tmux as described in long-running commands over SSH.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Only the first command runs | Local shell parsed ; or && before SSH |
Wrap the full remote command in single quotes: ssh host 'cmd1; cmd2' |
| Later commands never run | Earlier step failed in a && chain |
Use ; if every step must run, or fix the failing command |
cd affects unexpected later commands |
No subshell around the directory change | Wrap temporary cd in ( … ) |
| Variables expand on the wrong host | Double quotes on the client let the local shell expand $var |
Use single quotes around the remote command: ssh host 'echo "$HOME"' |
SSH hangs after cmd & |
Background job still tied to SSH stdio | Add wait, redirect I/O, or use detached execution |
| Here document runs locally | Missing ssh host before << |
The heredoc must be attached to the ssh command, not a bare shell block |
References
Summary
Multiple commands over SSH are shell chaining inside one remote command argument. Use semicolon when every step must run, && when later steps depend on success, and || for fallbacks. Wrap temporary cd commands in ( … ) so the working directory does not leak into the rest of the chain.
Use single quotes on the client when remote variables such as $HOME should expand on the server. When the commands live in a local file, run ssh host 'bash -s' < script.sh. When you paste several inline lines in the current shell, attach a here document to ssh. Background & stays inside one remote shell — it is not a substitute for detached long-running jobs.
Pick the operator that matches how failures should behave, keep the remote string in single quotes unless you deliberately want local expansion, and verify once with a harmless chain before you point the pattern at production paths.

