Library / SDK
spatie/ssh avatar
spatie/ssh

spatie/ssh turns the ssh binary into a fluent PHP API

A lightweight package to execute commands over an SSH connection

832 stars82 forksPHPMIT

At a glance

What is it?
spatie/ssh is a small PHP package that runs commands on a remote host over SSH and hands back a Symfony Process object, with options for ports, jump hosts, private keys, multiplexing, timeouts and file transfer over scp. It is deliberately thin, which is also its main limitation, and the README is candid about the two failure modes that will bite you: a missing sshpass and a Windows target that always reports success.
Who is it for?
Adopt spatie/ssh when your PHP application needs to run a handful of commands on a remote host and you want the SSH invocation wrapped in a fluent API instead of assembling ssh command lines yourself, and you are comfortable with a package that delegates to the system ssh and sshpass binaries. Do not adopt it as a general SSH client library, because the feature list stops at running commands and copying files.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 4 days ago.
What is it written in?
Mainly PHP, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 25, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One call, one Process object back

spatie/ssh is described as a lightweight package to execute commands over an SSH connection, and the whole surface fits in a couple of lines.

The entry point is a static create method that takes a user and a host:

php
Ssh::create('user', 'host')->execute('your favorite command');

What comes back is an instance of Symfony's Process, and that is the design decision that shapes everything else. You are not given a bespoke result object with the project's own accessors. You get the Process component, so isSuccessful() and getOutput() are the methods you already know from any other Symfony Process usage.

The alternative to waiting is executeAsync, which returns without waiting for the command to complete:

php
$process = Ssh::create('user', 'example.com')->executeAsync('your favorite command');

Multiple commands are a matter of passing an array, which the README describes as running multiple commands:

php
$process = Ssh::create('user', 'example.com')->execute([
   'first command',
   'second command',
]);

Behind the scenes, as the README puts it, all commands are performed using Symfony's Process. So the package is a layer that builds the right ssh invocation, applies the options you configured, and then hands the process over to a component you already depend on. There is no output parsing library, no connection pool and no retry logic of its own.

The licence is MIT, the package is on Packagist as spatie/ssh, and the release history is 1.13.0 in December 2024, 1.13.1 in November 2025 and 1.13.2 in July 2026, with the last push to the main branch on 2026-07-24.

Password authentication depends on sshpass being installed

The password story is where the README spends most of its warning, and the warning is worth taking literally.

You can pass a password to the constructor, in the form Ssh::create('user', 'host', port, 'password'), or set it later with usePassword('password').

The condition is that you have the sshpass package installed on the local system. The README is explicit that otherwise it will silently fail, which is the worst kind of failure: no exception, no error message, just a process that did nothing and reported success. If you are deploying this to a container or a CI image, sshpass has to be in the image, and checking for its presence is your job rather than the library's.

There is a security dimension to the same setting. Password authentication is enabled by default, and there is a method to turn it off:

php
Ssh::create('user', 'host')->disablePasswordAuthentication();

The sane configuration for anything beyond a throwaway script is a private key with password authentication disabled, and the package supports the key path directly:

php
Ssh::create('user', 'host')->usePrivateKey('/home/user/.ssh/id_rsa');

Quiet mode is a separate switch, disabled by default, and enableQuietMode() turns it on.

That setting is about output, not security, and it is worth knowing which one you are changing.

Ports, jump hosts and connection multiplexing

The connection options are the bulk of the API, and they are the parts that make the package useful in a real deployment rather than a demo.

A port is passed to the constructor or set with usePort, so Ssh::create('user', 'host', $port) and Ssh::create('user', 'host')->usePort($port) both work.

Jump hosts are configured with useJumpHost, and the argument is the familiar jump user, host and port triple in one string:

php
Ssh::create('user', 'host')->useJumpHost("$jumpuser@$jumphost:$jumpport");

That is a small thing that saves a lot of shell quoting, because the alternative in a PHP application is building an ssh invocation by hand.

Multiplexing is the option with the most operational value. If you are making many connections to the same host, SSH multiplexing enables re-using one TCP connection, and the method takes the control path and the persist value:

php
Ssh::create('user', 'host')->useMultiplexing($controlPath, $controlPersist);

// Ssh::create('user', 'host')->useMultiplexing('/home/.ssh/control_masters/%C', '15m');

For a job that opens a connection per host per minute, that is the difference between a handshake every time and one handshake for the whole run.

Timeouts are set with setTimeout, and the value in the example is 100, which the README shows as a plain number without further explanation, so the unit is whatever the underlying Process expects rather than something the package documents itself. If you are unsure, configure the process directly, which the README also shows:

php
Ssh::create('user', 'host')->configureProcess(fn (Process $process) => $process->setTimeout(null));

The switches that change how safe the connection is

Two defaults deserve attention because they are the opposite of what a convenience wrapper is often assumed to do.

Strict host key checking is enabled by default, and you can turn it off:

php
Ssh::create('user', 'host')->disableStrictHostKeyChecking();

Keeping it on is the right call. The check is what stops a host that presents an unexpected key from being accepted silently, and disabling it in production to make a first connection work is how that protection gets lost.

Password authentication is also enabled by default, as covered above, and disablePasswordAuthentication() exists for the same reason.

Quiet mode sits alongside them, disabled by default, and enableQuietMode() turns it on.

The pattern to notice is that the package defaults to the safer behaviour on the two security-relevant axes and leaves the ergonomics switches opt in. That is the opposite of the usual pattern in small wrappers, where the convenient default wins and the safe one is a method call you have to remember.

One more option exists for hosts that will not answer the fingerprint prompt in a non-interactive environment. It is worth knowing that it exists, and worth not using it outside a lab, because it removes the only check that a key belongs to the host you think it does.

File transfer runs through scp

Uploading and downloading are part of the package rather than something you bolt on with a separate library, and the implementation is stated plainly: under the hood the process will use scp.

php
Ssh::create('user', 'host')->upload('path/to/local/file', 'path/to/host/file');
php
Ssh::create('user', 'host')->download('path/to/host/file', 'path/to/local/file');

The method names follow the scp argument order, local path first on upload and remote path first on download, which matches what most people expect from the tool it wraps.

Because it is scp under the hood, the same credentials, the same key path, the same jump host and the same multiplexing settings apply, and the same caution about sshpass applies to password authentication. It also means the transfer is a process you can inspect, since the package hands you Process objects, and the same timeout behaviour applies to a large file as to a slow command.

That is the extent of the file handling, though. There is no recursive directory transfer in the documented API, no progress reporting and no resume, so for anything beyond a single file, the scp binary itself is still the more capable tool and the package is simply a convenient way to reach it from PHP.

Windows targets report the wrong exit code until you opt in

The onWindows method is the most useful thing the README explains, and it exists because of a specific behaviour of the Windows shell.

By default this package pipes your commands to bash on the remote host. On Windows the remote shell is cmd.exe, which handles input differently and always reports a successful exit code, even when a command fails. The onWindows method passes your commands as an argument to ssh instead, so cmd.exe runs them through cmd.exe /c and their real exit code, and therefore the result of isSuccessful(), is preserved.

php
Ssh::create('user', 'host')->onWindows()->execute('dir');

This is the kind of detail that turns a library from a convenience into something you can trust. A health check or a deployment step that reads isSuccessful() would otherwise pass on a Windows host every single time, silently, which is the worst possible outcome for an automated workflow.

Output handling is the other half of the API, and it is where the Process design pays off again. Passing a closure to onOutput gets you notified whenever the command produces output:

php
Ssh::create('user', 'host')->onOutput(function($type, $line) {echo $line;})->execute('whoami');

The closure receives two parameters. The type is Symfony's Process::OUT for regular output and Process::ERR for error output, and the line is the output itself. So a caller can stream standard output and standard error separately, which is what you want when a command writes progress to one stream and diagnostics to the other.

For testing, the README gives a single command, composer test, and the repository carries phpunit.xml.dist and a .php_cs.dist.php configuration for the code style.

Editorial conclusion

Adopt spatie/ssh when your PHP application needs to run a handful of commands on a remote host and you want the SSH invocation wrapped in a fluent API instead of assembling ssh command lines yourself, and you are comfortable with a package that delegates to the system ssh and sshpass binaries. Do not adopt it as a general SSH client library, because the feature list stops at running commands and copying files. Before you rely on it, confirm that sshpass is installed wherever you use password authentication, keep strict host key checking on unless you have a reason not to, and read the onWindows notes if any target runs cmd.exe, because the exit code you get there is wrong by default.

Frequently asked questions

How do I run a command over SSH with spatie/ssh?

Call Ssh::create('user', 'example.com')->execute('your favorite command'), which returns an instance of Symfony's Process, so isSuccessful() tells you whether it worked and getOutput() gives you the output. Use executeAsync if you do not want to wait for the command to finish.

What does spatie/ssh need for password authentication?

The sshpass package installed on the local system, and the README warns that without it the command will silently fail. Password authentication is enabled by default and can be turned off with disablePasswordAuthentication(), with a private key path given to usePrivateKey() as the alternative.

How do I upload or download files with spatie/ssh?

Use upload('path/to/local/file', 'path/to/host/file') and download('path/to/host/file', 'path/to/local/file'). The README states that under the hood the process will use scp, so the same credentials, jump host and multiplexing settings apply.

Why does every command succeed on my Windows target?

On Windows the remote shell is cmd.exe, which always reports a successful exit code even when a command fails, and the package pipes commands to bash by default. The onWindows method passes the command as an argument to ssh so cmd.exe runs it through cmd.exe /c and the real exit code is preserved.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. spatie/ssh on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/spatie-ssh.svg)](https://hysenlabs.com/projects/spatie-ssh)

Was this useful?