Skip to main content
Blog|
How-to guides

How to Run Commands Over SSH

|
Mar 18, 2026|11 min read
HOW-TO GUIDESHow to Run Commands Over SSHHOSTNEYhostney.comMarch 18, 2026

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.