We gave sshpass a wrong password and it exited 0. The login succeeded anyway, because ssh tried our key first and never reached the password prompt, so sshpass had nothing to send. That one result explains most of the confusion around sshpass: it doesn’t log you in, it only types a password when ssh asks for one, and its exit codes describe what happened at that prompt.
This guide uses sshpass for what it is good at, which is bootstrapping servers that only accept a password so you can push SSH keys to them once. You’ll pick a password source, run a key-push script, wrap scp, rsync and Ansible, and read the exit codes we measured. Everything ran in October 2026 on Rocky Linux 10.2 with sshpass 1.09 and on Debian 13.7 with sshpass 1.10. We did not test macOS; the Homebrew details below come from the formula itself.
Install sshpass
sshpass is a small C program in the main repositories of both distributions we tested, so there’s no third-party repo to add. The two versions differ in one option that matters later, so note which one you get.
| System | Install command | Version | Tested |
|---|---|---|---|
| Rocky Linux 10 | sudo dnf install -y sshpass | 1.09 (AppStream) | yes |
| Debian 13 | sudo apt install -y sshpass | 1.10 | yes |
| macOS | brew install sshpass | 1.10 (homebrew-core) | no |
The macOS row needs a word of caution. For years sshpass was only available on macOS through third-party taps, and many older guides still tell you to add one. The formula is now in homebrew-core at 1.10, so plain brew install sshpass is the route. Its metadata lists prebuilt bottles for Apple Silicon on current releases, but Intel bottles only for Sonoma, Ventura and Monterey, so an Intel Mac on Sequoia or Tahoe builds it from source. We have not run it on a Mac, so treat everything else in this guide as tested on Linux only. Check what you installed.
sshpass -V
Rocky Linux 10 printed the 1.09 banner, including the prompt string sshpass watches for.
sshpass 1.09
(C) 2006-2011 Lingnu Open Source Consulting Ltd.
(C) 2015-2016, 2021 Shachar Shemesh
This program is free software, and can be distributed under the terms of the GPL
See the COPYING file for more information.
Using "assword" as the default password prompt indicator.
That last line is the whole mechanism. sshpass runs ssh in a pseudo-terminal, waits for output containing assword, and types the password. Anything that changes the prompt or skips it changes what sshpass does.
Allow password logins on the target
sshpass can’t help if the server refuses passwords, and many do: cloud images and hardened servers switch them off. Check what the target’s sshd really enforces rather than grepping one file, because drop-in files in /etc/ssh/sshd_config.d/ can override it:
sudo sshd -T | grep -E '^(passwordauthentication|authenticationmethods) '
You need passwordauthentication yes, and authenticationmethods must be any or include password. If either blocks you, add a drop-in that sorts before the others and sets both, then reload sshd. The unit is sshd on Rocky and ssh on Debian. Our Rocky Linux SSH server guide explains why the file name decides which setting wins:
printf 'PasswordAuthentication yes\nAuthenticationMethods any\n' | sudo tee /etc/ssh/sshd_config.d/05-allow-password.conf
sudo systemctl reload sshd
sudo sshd -T | grep -E '^(passwordauthentication|authenticationmethods) '
On our Rocky server the check then printed passwordauthentication yes and authenticationmethods any. While that file exists, the server takes passwords from anyone who can reach it, so on an internet-facing host keep the window short, ideally with SSH limited to your address in the firewall. The last section shows how to remove it.
Pick how sshpass reads the password
sshpass takes the password from exactly one source. All of them worked against our Rocky target, and our ranking from safest to riskiest is:
-d: an inherited file descriptor, such as an anonymous pipe-f: the first line of a file with mode 600-e: an environment variable,SSHPASSby default-p: the password on the command line
For scripts, -f is the practical default. In the commands below, [email protected] stands for your own account and server, the account whose password you want sshpass to type. The first line creates an Ed25519 key if you don’t have one yet, which the key-push section needs and which also creates ~/.ssh with the right mode. Then create the password file with tight permissions before writing the password into it, so it is never readable by others even for a moment. read -rsp prompts for the password and reads it without echoing, which keeps it out of your shell history; type it and press Enter. StrictHostKeyChecking=accept-new matters on a first contact: without it, our first sshpass login to a host missing from known_hosts stopped at the yes/no prompt and exited 6 without a word, while this form recorded the key and logged in:
[ -f ~/.ssh/id_ed25519 ] || ssh-keygen -t ed25519
install -m 600 /dev/null ~/.ssh/deploy.pass
read -rsp 'Password: ' PW && echo && printf '%s\n' "$PW" > ~/.ssh/deploy.pass && unset PW
sshpass -f ~/.ssh/deploy.pass ssh -o StrictHostKeyChecking=accept-new [email protected] 'echo file method works'
The -d form keeps the password off the disk entirely. Bash process substitution hands sshpass a pipe on descriptor 3, and reading the password with the same prompt keeps it out of your history as well:
read -rsp 'Password: ' PW && echo
sshpass -d 3 ssh -o StrictHostKeyChecking=accept-new [email protected] 'echo fd method works' 3< <(printf '%s\n' "$PW")
unset PW
We checked what other users can actually see, since that is the usual objection to -p and -e. With -p, sshpass 1.09 overwrites its own argument once it starts, so ps one second into the login showed a masked value:
PID COMMAND
8133 sshpass -p xxxxxxxxxxxxx ssh [email protected] sleep 3
That masking happens after the process starts, so there is a window where the real password is visible, and it lands in your shell history unless HISTCONTROL includes ignorespace and you start the line with a space. With -e, the variable sat in /proc/<pid>/environ for the same user, while another user’s cat of that file got Permission denied. Neither is a disaster on a single-user workstation, and both are a bad idea on a shared jump host.
sshpass 1.10 on Debian can read a variable with another name, but the syntax is unforgiving. sshpass -eDEPLOY_PASS, with no space, worked. With a space, 1.10 appears to have treated DEPLOY_PASS as the command to run, complained that SSHPASS was not set and then crashed with a segmentation fault, exit code 139. Rocky’s 1.09 has no named form at all.
Push SSH keys to a list of servers
Deploying keys is the job sshpass is best at, because it makes itself unnecessary: after one password login per host, every later connection uses the key. Put your hosts in a file, one per line.
printf '%s\n' 10.0.1.50 10.0.1.99 > ~/servers.txt
Then create the script that walks the list, so you can rerun it whenever new servers arrive.
vim ~/push-keys.sh
This is the exact script we ran. It reads the password file from the previous step and prints one result line per host, so a failure on one server doesn’t stop the rest:
#!/bin/bash
set -u
PASS_FILE="$HOME/.ssh/deploy.pass"
SERVER_LIST="$HOME/servers.txt"
REMOTE_USER="deploy"
PUBKEY="$HOME/.ssh/id_ed25519.pub"
while read -r server; do
[ -z "$server" ] && continue
if sshpass -f "$PASS_FILE" ssh-copy-id -i "$PUBKEY" \
-o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new \
"${REMOTE_USER}@${server}" < /dev/null > /dev/null 2>&1; then
echo "OK $server"
else
echo "FAIL $server (exit $?)"
fi
done < "$SERVER_LIST"
Two details are deliberate. < /dev/null stops ssh-copy-id from swallowing the rest of servers.txt from standard input, which otherwise makes the loop end after the first host. StrictHostKeyChecking=accept-new records each new host key without a prompt but still refuses a host whose key changed, which is safer than the StrictHostKeyChecking=no most sshpass examples use. Make it executable and run it.
chmod 700 ~/push-keys.sh
~/push-keys.sh
Our list held one real server and one address with nothing behind it. The whole run took 10.4 seconds, most of it the 5 second connect timeout on the dead host:

Once every host shows OK, log in once without sshpass to prove the key works, and leave password authentication on until you have finished with the rest of this guide. If you want the key login itself explained, our guide to using SSH keys on Debian covers generating, loading and naming them. Keep the password file and password logins in place for now, because the next two sections still use them.
Copy files with scp and rsync
sshpass wraps any command that ends up asking ssh for a password, so the outer command can be scp or rsync rather than ssh. Both worked on our first attempt. We added -o PubkeyAuthentication=no because the key from the previous section was already on the server, and without it ssh would have used the key and sshpass would have done nothing. Leave it out on a server that has no key for you yet. For scp, you put sshpass in front.
sshpass -f ~/.ssh/deploy.pass scp -o PubkeyAuthentication=no ~/site/index.html [email protected]:/tmp/
rsync works the same way. sshpass wraps the whole rsync call, and the ssh process rsync starts inherits the terminal sshpass is watching:
sshpass -f ~/.ssh/deploy.pass rsync -av -e "ssh -o PubkeyAuthentication=no" ~/site/ [email protected]:site/
The transfer reports like any rsync run you have seen.
sending incremental file list
created directory site
./
index.html
css/
css/style.css
sent 257 bytes received 96 bytes 706.00 bytes/sec
total size is 22 speedup is 0.06
Our rsync examples guide covers the flags for mirrors and backups once the copy itself works.
Give Ansible a password, with or without sshpass
Whether Ansible needs sshpass depends on its version, and the answer flipped in ansible-core 2.19. That release added a password_mechanism setting to the ssh connection plugin with three choices, ssh_askpass, sshpass and disable, and made ssh_askpass the default. We tested both generations on the same Rocky control node:
- ansible-core 2.16 (Rocky 10 AppStream): sshpass required
- ansible-core 2.21.5 (pip, default settings): password login with sshpass uninstalled
- ansible-core 2.21.5 with
ANSIBLE_SSH_PASSWORD_MECHANISM=sshpass: sshpass required again
Without sshpass, the distribution’s 2.16 failed before it even connected:
10.0.1.50 | FAILED! => {
"msg": "to use the 'ssh' connection type with passwords or pkcs11_provider, you must install the sshpass program"
}
Our inventory was a small INI file, and yours needs only your own host addresses in it.
vim inventory.ini
It names the host group and the remote user.
[new_servers]
10.0.1.50
[new_servers:vars]
ansible_user=deploy
On ansible-core 2.21.5 from pip, with sshpass removed and no key on the target account, a plain password ping worked. Ansible answers ssh’s password prompt itself through SSH_ASKPASS, so nothing extra is needed on a current control node:
ANSIBLE_HOST_KEY_CHECKING=False ansible new_servers -i inventory.ini -m ping -e ansible_password=StrongPass123
It returned "ping": "pong" with exit code 0, plus a warning about the discovered Python interpreter. If you already pushed your key in the section above, that ping succeeds through the key instead; add the PubkeyAuthentication=no argument shown further down to test the password path. Interactively, --ask-pass (-k) prompts for the password instead of putting it on the command line. If you set ANSIBLE_SSH_PASSWORD_MECHANISM=sshpass (or password_mechanism = sshpass under [ssh_connection] in ansible.cfg) to keep the old behaviour, 2.21.5 asks for sshpass again and fails without it:
10.0.1.50 | FAILED! => {
"changed": false,
"msg": "Task failed: to use the password_mechanism=sshpass, you must install the sshpass program"
}
Our 2.16 test ran against an account that already held our key, so we forced the password path with an extra variable. Watch the quoting if you do the same: passing ansible_ssh_common_args as key=value made Ansible split the value at the space, and ssh failed with no argument after keyword "-o". The JSON form keeps it intact and returned pong:
ANSIBLE_HOST_KEY_CHECKING=False ansible new_servers -i inventory.ini -m ping -e ansible_password=StrongPass123 -e '{"ansible_ssh_common_args": "-o PubkeyAuthentication=no"}'
Our guide to installing Ansible on Rocky Linux and Ubuntu sets up the control node itself, and its ansible --version tells you which of these behaviours you get.
Read the sshpass exit codes we measured
The sshpass manual lists exit codes 0 to 7, and scripts branch on them. We triggered each case on purpose, and two of them would surprise anyone reading only the list of codes:
| Situation | Exit code | What it means |
|---|---|---|
| Login worked | 0 | Success, or a key logged in before any password prompt |
Unknown option (-z) | 1 | Invalid argument |
-e and -p together | 2 | Conflicting password sources |
| Wrong password, no key | 5 | Password rejected |
| Right password, account refuses passwords (root) | 5 | Same code as a wrong password |
| Host key not in known_hosts | 6 | sshpass stopped at the yes/no prompt |
| Host key changed | 255 | ssh’s own failure; the manual’s code 7 never appeared |
| Port closed or host unreachable | 255 | ssh could not connect |
Wrong -P prompt string | hang | sshpass never saw its prompt; we stopped it with timeout (124) |
Three rows deserve a reason. Exit 0 with a wrong password happens whenever ssh has a usable key, because sshpass only reacts to a prompt that never came, so add -o PubkeyAuthentication=no when you are testing a password. A changed host key gave 255 on both 1.09 and 1.10, and the manual explains it: that complaint does not go to the terminal sshpass watches, so ssh’s own return code (typically 255) is passed back. Code 7 is reserved for a changed IP key under CheckHostIP. Exit 5 can’t tell a typo from a server rule such as PermitRootLogin prohibit-password, which is worth remembering before you lock an account by retrying.
Speed is not a reason to avoid sshpass: ten password logins through sshpass took 1.71 seconds against 1.45 seconds for ten key logins on the same host, about 26 ms extra each, and that gap is password authentication against key authentication rather than sshpass overhead alone. The reasons to keep it short-lived are the ones above, a password sitting in a file or a variable and a server that has to accept passwords at all. Use it to deploy keys or to reach a box that can’t take one, then switch to keys, and if a host-key prompt is what blocks your automation, our note on SSH host key checking explains the safer options. For the protocol and key management in depth, the SSH books we recommend are the next read.
When you have finished with every section above, undo the bootstrap state on both sides. On the machine you ran sshpass from, delete the password file:
rm -f ~/.ssh/deploy.pass
Then, on every server in servers.txt where you added the password drop-in, remove it and reload sshd (ssh on Debian) so password logins are off again:
sudo rm /etc/ssh/sshd_config.d/05-allow-password.conf
sudo systemctl reload sshd
sudo sshd -T | grep -E '^passwordauthentication '
On our Rocky server the last line printed passwordauthentication no again, because the hardening file underneath took over once the drop-in was gone. On a server where nothing else turns passwords off, set PasswordAuthentication no in your own drop-in instead of just deleting this one.