You do not need to open an interactive shell on a remote server every time you want to run a command. SSH lets you pass a command directly, execute it on the remote machine, and return the output to your local terminal. The connection opens, the command runs, and the connection closes. No shell prompt, no manual typing on the server.
ssh user@example.com "uptime"
This connects to the server, runs
uptime
, prints the output locally, and disconnects. The entire round trip takes a couple of seconds.
This is useful for quick checks, scripting, automation, and any situation where opening an interactive session is more overhead than needed. This guide covers the syntax, practical examples, and the edge cases you will run into.
Basic syntax#
ssh user@host "command"
The command goes in quotes after the host. SSH opens a connection, authenticates, runs the command in a non-interactive shell on the remote machine, streams the output back to your terminal, and exits when the command finishes.
ssh john@example.com "hostname"
Output:
example.com
The quotes are technically optional for simple single-word commands, but always use them. Without quotes, your local shell may interpret parts of the command before SSH sends it to the remote machine.
Specifying a port or key#
If the server uses a non-standard SSH port:
ssh -p 2222 user@example.com "command"
With a specific SSH key:
ssh -i ~/.ssh/id_ed25519 user@example.com "command"
Both together:
ssh -p 2222 -i ~/.ssh/id_ed25519 user@example.com "command"
If you connect to this server frequently, save these options in your SSH config file (
~/.ssh/config
) so you do not have to type them every time. See What port does SSH use for SSH config setup.
Running multiple commands#
Sequential commands with semicolons
ssh user@example.com "cd /var/www/html; ls -la"
Both commands run in sequence. The semicolon separates them, same as on a local shell. The second command runs regardless of whether the first one succeeded.
Sequential commands with && (stop on failure)
ssh user@example.com "cd /var/www/html && ls -la"
With
&&
, the second command only runs if the first one succeeds. If
cd
fails (directory does not exist),
ls
does not run. This is usually what you want.
Sequential commands with || (run on failure)
ssh user@example.com "test -f /var/log/app.log || echo 'Log file does not exist'"
With
||
, the second command runs only if the first one fails. Useful for fallback logic and conditional checks.
Combining multiple commands
ssh user@example.com "cd /var/www/html && echo 'Current directory:' && pwd && echo 'Disk usage:' && df -h ."
You can chain as many commands as you need. The quotes keep everything together as a single command string sent to the remote shell.
Using heredoc for multi-line commands#
For anything longer than a couple of commands, a heredoc is more readable than cramming everything onto one line:
ssh user@example.com << 'EOF'
cd /var/www/html
echo "Files in web root:"
ls -la
echo ""
echo "Disk usage:"
df -h .
echo ""
echo "PHP version:"
php -v
EOF
The single quotes around
EOF
are important. They prevent your local shell from expanding variables in the heredoc. Without the quotes,
$variables
in the command block would be expanded locally before being sent to the remote machine, which is almost never what you want.
If you do want local variable expansion (for example, to pass a local value to the remote command):
LOCAL_FILE="backup.sql"
ssh user@example.com << EOF
ls -la /home/user/$LOCAL_FILE
EOF
Without quotes around EOF,
$LOCAL_FILE
is expanded locally to
backup.sql
before the command is sent.
Piping output#
Pipe remote output to local commands
The output of a remote command comes back through stdout, so you can pipe it into local commands:
ssh user@example.com "cat /var/log/nginx/error.log" | grep "502"
This runs
cat
on the remote server and pipes the output to
grep
on your local machine. Useful when you want to process remote data with local tools.
ssh user@example.com "mysqldump database_name" > local_backup.sql
This runs
mysqldump
on the remote server and redirects the output to a file on your local machine. A common way to pull database backups without needing to create a file on the server first.
Pipe local input to remote commands
cat local_script.sql | ssh user@example.com "mysql database_name"
This sends a local SQL file to the remote server’s mysql client. The local file is piped through the SSH connection into the remote command’s stdin.
echo "SELECT COUNT(*) FROM wp_posts;" | ssh user@example.com "mysql wordpress_db"
Pipe between two remote servers
ssh user@server1 "mysqldump database_name" | ssh user@server2 "mysql database_name"
This dumps a database from server1 and pipes it directly into server2’s mysql client. The data flows through your local machine but never touches disk. Useful for database migrations between servers.
Running scripts remotely#
Run a local script on a remote server
ssh user@example.com "bash -s" < local_script.sh
The
-s
flag tells bash to read commands from stdin. Your local script is streamed through the SSH connection and executed on the remote machine. The script does not need to exist on the remote server.
With arguments:
ssh user@example.com "bash -s" < local_script.sh arg1 arg2
Run a script that already exists on the server
ssh user@example.com "bash /home/user/scripts/deploy.sh"
Or if the script is executable:
ssh user@example.com "/home/user/scripts/deploy.sh"
Run a one-liner script
ssh user@example.com "for f in /var/log/*.log; do echo \$f: \$(wc -l < \$f) lines; done"
Note the escaped dollar signs (
\$
). Without the backslashes, your local shell expands
$f
before SSH sends the command, and the remote server receives empty variables. Escaping tells your local shell to pass
$f
literally so the remote shell interprets it.
Alternatively, use single quotes to prevent all local expansion:
ssh user@example.com 'for f in /var/log/*.log; do echo $f: $(wc -l < $f) lines; done'
Single quotes pass everything through literally. No escaping needed.
Practical examples#
Check disk space
ssh user@example.com "df -h"
Check memory usage
ssh user@example.com "free -h"
Tail a log file
ssh user@example.com "tail -100 /var/log/nginx/error.log"
For real-time log watching, use
-f
but note that this keeps the SSH connection open until you press Ctrl+C:
ssh user@example.com "tail -f /var/log/nginx/error.log"
Restart a service
ssh user@example.com "sudo systemctl restart nginx"
ssh user@example.com "sudo systemctl restart php8.2-fpm"
Check running processes
ssh user@example.com "ps aux | grep php"
The pipe here runs on the remote machine because the entire string inside the quotes is executed remotely. Both
ps
and
grep
run on the server.
Check if a website is responding
ssh user@example.com "curl -sI http://localhost | head -5"
Useful for checking if a web server is responding without going through external DNS or CDN layers.
Run WP-CLI commands remotely
ssh user@example.com "cd /var/www/html && wp plugin list --status=active"
ssh user@example.com "cd /var/www/html && wp cache flush"
ssh user@example.com "cd /var/www/html && wp db export - " > local_backup.sql
The last example exports the WordPress database and saves it to a local file. WP-CLI’s
wp db export -
outputs to stdout, which gets piped through SSH to your local redirect.
ssh user@example.com "cd /var/www/html && wp search-replace 'old-domain.com' 'new-domain.com' --dry-run"
The
--dry-run
flag shows what would change without making changes. Remove it when you are ready to commit.
Find large files
ssh user@example.com "find /var/www/html -type f -size +50M -exec ls -lh {} \;"
Check WordPress file permissions
ssh user@example.com "find /var/www/html -type f ! -perm 644 -exec ls -la {} \;"
Finds files that do not have the standard 644 permissions. Useful for security audits.
Compress and download a directory
ssh user@example.com "tar -czf - /var/www/html/wp-content/uploads" > uploads.tar.gz
The
-
tells tar to write to stdout instead of a file. The compressed archive streams through SSH to a local file. No temporary file is created on the server.
For transferring individual files, SCP is simpler. For interactive file management, use SFTP.
Exit codes#
When you run a command over SSH, the exit code of the remote command becomes the exit code of the SSH command. This makes SSH commands work naturally in scripts and conditional logic:
ssh user@example.com "test -f /var/www/html/wp-config.php"
echo $?
If the file exists,
test
returns 0 and so does the SSH command. If the file does not exist, both return 1.
if ssh user@example.com "test -d /var/www/html"; then
echo "Web root exists"
else
echo "Web root is missing"
fi
This works because
if
checks the exit code of the SSH command, which reflects the exit code of the remote
test
.
If SSH itself fails to connect, it returns 255. This lets you distinguish between “command failed on the server” (non-zero exit code from the command) and “could not reach the server” (exit code 255). See SSH connection refused for diagnosing connection failures.
Running commands in the background#
Remote command, detached from SSH
If you want a command to keep running after the SSH connection closes:
ssh user@example.com "nohup /home/user/scripts/long_task.sh > /tmp/task.log 2>&1 &"
nohup
prevents the process from receiving a hangup signal when SSH disconnects. The
&
puts it in the background. Output goes to a log file because there is no terminal to write to after SSH disconnects.
Without
nohup
, background processes are killed when the SSH session ends.
Using screen or tmux
For long-running interactive tasks where you want to reconnect later:
ssh user@example.com "screen -dmS mytask /home/user/scripts/long_task.sh"
This starts a detached screen session named
mytask
. Reconnect later with:
ssh user@example.com "screen -r mytask"
tmux works the same way:
ssh user@example.com "tmux new-session -d -s mytask '/home/user/scripts/long_task.sh'"
Quoting and escaping#
Quoting is the most common source of confusion with remote SSH commands. The problem: your local shell processes the command string before SSH sends it. If you are not careful, variables get expanded locally, special characters get interpreted locally, and the remote server receives something different from what you intended.
Double quotes: local shell expands variables
ssh user@example.com "echo $HOME"
This prints your local home directory, not the remote one. Your local shell expands
$HOME
before SSH sends the command.
Single quotes: nothing is expanded locally
ssh user@example.com 'echo $HOME'
This prints the remote home directory. Single quotes prevent local expansion, so
$HOME
is sent literally to the remote shell.
Escaping inside double quotes
ssh user@example.com "echo \$HOME"
The backslash escapes the dollar sign from local expansion. The remote shell receives
echo $HOME
and expands it there.
Nested quotes
When the remote command itself needs quotes:
ssh user@example.com "grep 'error' /var/log/nginx/error.log"
This works because single quotes inside double quotes are passed through to the remote shell.
For double quotes inside double quotes, escape them:
ssh user@example.com "echo \"hello world\""
Or use single quotes on the outside:
ssh user@example.com 'echo "hello world"'
When in doubt, use heredoc
If the quoting gets complicated, a heredoc with quoted delimiter avoids the problem entirely:
ssh user@example.com << 'EOF'
echo "Home: $HOME"
echo "User: $(whoami)"
echo "Date: $(date)"
EOF
Everything between the delimiters is sent to the remote shell without any local interpretation.
SSH command over a non-interactive shell#
When you run a command via SSH (as opposed to opening an interactive session), the remote shell is non-interactive. This has a few consequences:
Shell profile files may not load. Depending on the remote shell and OS, files like
.bashrc
,
.bash_profile
, or
.profile
may not be sourced. If a command depends on environment variables, PATH additions, or aliases defined in those files, it may fail or behave differently than expected.
If you need the remote environment to be fully initialized:
ssh user@example.com "source ~/.bashrc && wp plugin list"
Or use a login shell explicitly:
ssh user@example.com "bash -l -c 'wp plugin list'"
The
-l
flag tells bash to run as a login shell, which sources the profile files.
No TTY by default. Commands run via SSH do not get a pseudo-terminal unless you request one. Most commands do not need one, but some interactive tools (like
top
,
htop
, or anything that uses ncurses) do.
Force a TTY allocation with
-t
:
ssh -t user@example.com "top"
For commands that need a TTY for sudo password prompts:
ssh -t user@example.com "sudo systemctl restart nginx"
Some sudo configurations require a TTY. If
sudo
fails with “no tty present,” add
-t
.
Automating with cron#
SSH commands can be scheduled with cron on your local machine or on the remote server itself.
Run a remote command on a schedule from your local machine:
0 2 * * * ssh user@example.com "cd /var/www/html && wp cron event run --due-now" >> /var/log/remote-wp-cron.log 2>&1
This requires passwordless SSH key authentication – cron cannot enter a password. See the SSH keys guide for setup.
Run commands directly on the server’s cron:
If you have SSH access to the server, you can edit the remote crontab directly:
ssh user@example.com "crontab -l"
This lists the current cron jobs on the remote server. For more on cron syntax and scheduling, see the cron jobs guide.
SSH commands on Hostney#
On Hostney, every account has SSH access inside an isolated container. You can run commands remotely the same way as any other SSH server.
Connect using:
- Host: your server hostname from the control panel
- Port: 22
- Username: your account username
- Authentication: SSH key (configured through the SSH Keys section in the control panel)
Example:
ssh user@your-server.hostney.com "cd /var/www/html && wp plugin list --status=active"
SSH key authentication is required. Configure your key through the Terminal Access section in the control panel.
Summary#
SSH runs commands remotely without opening an interactive shell. Use quotes to wrap the command, semicolons or
&&
to chain multiple commands, heredocs for multi-line scripts, and pipes to stream data between local and remote machines. Watch your quoting – single quotes prevent local variable expansion, double quotes allow it. For long-running tasks, use
nohup
or screen/tmux to keep processes alive after SSH disconnects. Exit codes pass through, so SSH commands work naturally in scripts and conditionals.