spatie/ssh
spatie/ssh is a lightweight PHP package for running commands over an SSH connection. Execute commands synchronously or asynchronously, run multiple commands, choose a custom port, and inspect results via Symfony's Process.
Architecture fit. spatie/ssh is a deliberately thin wrapper: it shells out to the host's ssh/scp binaries via Symfony Process and hands the Process object back to you. There is no PHP SSH protocol implementation, no ext-ssh2/libssh dependency, no daemon, and no service provider semantics beyond a facade. For a TPM this is the key architectural decision point: you inherit everything the local OpenSSH client gives you (~/.ssh/config conventions, known_hosts, jump hosts, agent auth, multiplexing) and you inherit everything the local OpenSSH client requires of you (binaries present, writable paths, correct POSIX shell availability on the target). It fits deployment runbooks, scheduled maintenance jobs, remote artisan invocation, and small-fleet orchestration from a Laravel app or CI runner. It does not fit interactive sessions, sustained high-volume transfers, or anything needing SFTP semantics, port forwarding, or structured protocol-level error handling.
Integration feasibility. composer require spatie/ssh, then Ssh::create('user', 'host')->execute('...'). A developer can be productive in under an hour. The real cost sits outside the package: SSH key material and its distribution, known_hosts management, host inventory, and command construction hygiene. Verify whether the facade alias is auto-discovered or must be registered manually, and confirm the transitive symfony/process constraint overlaps with the Symfony version your Laravel release pins.
Technical risk.
sshpass on the local host and passes the secret on a command line, where it is visible in process listings. Treat password auth as unsupported and mandate key-based auth.sshpass. A tooling layer that can fail without throwing is a category of risk, not a bug. Every call site needs an isSuccessful() check plus output capture fed into logging.bash on the target; Windows targets historically reported success regardless of outcome until 1.13.2's onWindows(). Cross-platform fleets need explicit opt-in and verification.execute() is documented but its failure semantics (does command 2 run after command 1 fails?) are not stated. Must be validated in a spike before it is relied on for sequencing.Key questions.
onWindows() and its recent, less-exercised code path)?scp semantics (rsync, resume, large directories)?known_hosts and jump-host configuration, and is disableStrictHostKeyChecking() banned by policy?exec('ssh ...'), or phpseclib?Stack fit. Composer-installable, Laravel-native ergonomics, no runtime services. It lives naturally inside queued jobs, artisan commands, and schedule:run. It is a poor fit for a synchronous HTTP path where a slow remote host becomes a slow user request. Container images must be updated to include openssh-client (and sshpass only if the risky path is ever enabled); slim PHP base images routinely omit both.
Migration path. From Envoy: near-identical mental model, since the Ssh class derives from Envoy's own code — porting runbooks is largely mechanical, but Envoy's @task/@story orchestration must be replaced with your own layer. From raw exec('ssh ...'): strictly better, since you gain Process output, exit codes, timeouts, and onOutput streaming. Away from DivineOmega/php-ssh-connection or phpseclib: not a drop-in; you lose in-process SFTP and structured error handling and gain external-binary dependency. To keep the exit door open, wrap the package behind an internal interface (e.g. RemoteHost::run(string $command): CommandResult) — this is cheap now and expensive later.
Compatibility. Confirm PHP floor and Symfony Process constraint against your Laravel version. SSH multiplexing requires a writable control-path directory that persists across invocations — acceptable on a stable VM, problematic in ephemeral containers where stale control sockets accumulate or the path disappears between jobs. IPv6 support and jump hosts are present but should be smoke-tested in your specific network topology.
Sequencing.
Ssh::create() directly.disableStrictHostKeyChecking().Maintenance. Small surface area (essentially one class) means low maintenance, but you own the wrapper, the escaping rules, and the host inventory. Pin with caret constraints and review the changelog on every bump — recent history is dominated by Symfony-version churn and exit-code fixes rather than feature creep. Budget a recurring half-day per quarter for dependency hygiene; near-zero for the package, most of it for the Symfony constraint.
Support. Community OSS with no SLA. Security reports go to [email protected]; nothing else is contractual. Assign a named internal owner and a documented fork procedure. Do not place this on a path where an unfixed upstream bug blocks revenue.
Scaling. Each execute() is a blocking process spawn plus a TCP handshake — roughly linear cost per host. Enabling multiplexing changes that calculus meaningfully for repeated calls to the same host and should be the default once the control-path lifetime problem is solved per worker. For fan-out beyond a handful of hosts, use executeAsync() plus wait() with a bounded concurrency, or better, one queued job per host so Laravel's queue concurrency and retry semantics apply. Large outputs buffer in memory inside Process — stream via onOutput and cap retention rather than getOutput() on verbose commands.
Failure modes to design for.
sshpass/openssh-client → failure without exception; add a startup health check.known_hosts refresh runbook.Ramp-up. One to two days for a mid-level Laravel developer to be productive; one to two weeks to build and validate the guardrails (escaping, inventory, audit logging, retry and timeout policy, CI harness). The adoption cost is not learning the API — it is accepting that a shell-out tool transfers all operational responsibility for connectivity, credentials, and command safety onto your team. For internal ops tooling at modest scale, that trade is favorable and reversible. For a customer-facing or high-blast-radius path, gate it behind the adapter interface and a documented exit to phpseclib or an agent-based configuration management system.
How can I help you explore Laravel packages today?