The recent announcement from Epic Castle introduces ClojureSSH, a new library that wraps the JSch SSH client in idiomatic Clojure. This release is not just another wrapper; it represents the second step in a deliberate effort to break apart the monolithic Spire codebase into focused, reusable components. The first piece of that effort was bbssh, which provided a thin Clojure façade over the native ssh binary. By extracting the SSH functionality that had lived inside Spire for years and polishing it into a standalone library, the team aims to give developers a lightweight, dependency‑free way to perform secure remote operations directly from the JVM. This modular approach reduces coupling, simplifies versioning, and makes it easier for the community to contribute to each piece independently.
ClojureSSH was deliberately designed to share its exact API with bbssh. The namespaces, function names, and signatures are identical, which means that code written against one library can be swapped for the other without any changes. Moreover, the version numbers are kept in lockstep: the inaugural release of ClojureSSH is 0.7.0, matching bbssh 0.7.0, and future updates will continue this dual‑release strategy. This version synchrony eliminates the guesswork that often accompanies maintaining parallel libraries and provides a clear migration path for teams that already rely on bbssh but wish to drop the requirement for a local ssh executable.
For traditional Clojure projects, adding ClojureSSH is as simple as including the appropriate coordinates in your deps.edn file. Once on the classpath, you can call functions such as clojuressh.core/ssh‑connect or clojuressh.core/exec without needing to install or configure an external ssh binary on the host machine. Because the library talks directly to JSch, it works on any platform that supports a Java runtime, making it ideal for containerized environments, CI pipelines, or Windows developers who would otherwise have to manage OpenSSH binaries. The absence of native executables also reduces attack surface and eliminates platform‑specific deployment headaches.
One of the most intriguing aspects of ClojureSSH is its seamless integration with Babashka. When used inside a Babashka script, the library acts as a shim that forwards calls to the existing bbssh pod, allowing you to add it as a regular dependency in deps.edn and invoke the same functions you would use in a full Clojure JVM. This eliminates the need for reader conditionals, manual pod loading, or duplicate code paths. Whether you are writing a pure Clojure application or a lightweight Babashka automation script, you can now rely on a single SSH implementation that behaves identically across both contexts, streamlining maintenance and reducing cognitive overhead.
When comparing ClojureSSH to the older clj‑ssh library, the differences become most apparent in the handling of SCP operations. In earlier versions of Spire, the team relied on clj‑ssh for file transfers but encountered consistent issues where timestamps and file permissions were not preserved after a copy. The SCP implementation bundled with ClojureSSH originates from the battle‑tested code inside Spire, where it has been refined through years of real‑world usage. Consequently, ClojureSSH retains metadata such as modification times, access times, and Unix permission bits, ensuring that mirrored directory structures remain faithful to the source. This reliability is crucial for deployment scripts, backup utilities, and any workflow where file integrity matters.
Beyond faithful metadata preservation, ClojureSSH offers a progress‑fn callback that can be supplied to SCP‑related functions. This callback receives periodic updates indicating the number of bytes transferred, enabling developers to build responsive user interfaces, display progress bars, or log transfer milestones in long‑running batch jobs. The ability to monitor copy progress transforms a previously opaque operation into a transparent one, improving observability and allowing operators to intervene quickly if a transfer stalls or exceeds expected thresholds.
User interaction is another area where ClojureSSH mimics the behavior of the native ssh client. When a private key requires a passphrase, or when a password is needed for authentication, the library prompts the user through the standard input stream, just as you would see in a terminal session. Likewise, when encountering an unknown host key, ClojureSSH asks whether to trust the key and, upon acceptance, automatically adds it to the known hosts file. This approach eliminates the need for developers to manually manage credential callbacks or host verification logic, reducing boilerplate while maintaining secure defaults that align with familiar SSH workflows.
The library also brings first‑class support for SSH configuration files via the clojuressh.config-repository namespace. By parsing ~/.ssh/config, it picks up host aliases, custom port numbers, IdentityFile directives, and other user‑defined settings. This means that you can refer to a host by its shorthand name (e.g., “web01”) and have the library automatically resolve the appropriate hostname, port, and key file without extra code. For teams that already maintain rich SSH config files across developer workstations and servers, this feature ensures a smooth transition to programmatic SSH without duplicating configuration logic.
Interactive sessions benefit from the clojuressh.terminal namespace, which handles raw terminal mode, negotiates window size changes, and provides readline‑style input handling. These utilities are essential when building remote consoles, interactive debugging tools, or any application that needs to emulate a full‑featured terminal over SSH. By abstracting away the low‑level terminal mechanics, the library lets developers focus on the logic of their interactive programs while still delivering a responsive, correctly sized user experience that adapts to resizing events.
ClojureSSH also enjoys tight interoperation with Babashka’s process library. The core/exec function accepts threading macros (‑>) that allow you to pipe data between local babashka.process streams and remote SSH commands. For example, you can take the stdout of a local command, stream it directly into a remote stdin, capture the remote stdout, and feed it back into another local process—all within a single expression. This capability opens up powerful patterns such as distributed data processing, remote log aggregation, or chaining together heterogeneous tools across machine boundaries without writing temporary files or managing complex socket plumbing.
Users running JDK 22 or newer may notice a warning the first time the JNA‑based terminal code loads a native library. This warning originates from the JVM’s stricter native‑access controls and is not an error; it simply reflects that the library is attempting to access unnamed modules. The README provides two straightforward remedies: launch the JVM with –enable-native-access=ALL-UNNAMED to silence the warning, or invoke a tiny piece of code early in the startup sequence to trigger the load and print the warning once, after which subsequent loads remain quiet. Understanding this nuance prevents alarm during deployment and ensures that the warning does not confuse end‑users who might mistake it for a failure.
For developers considering adoption, the practical advice is to start by integrating ClojureSSH into non‑critical automation scripts or internal tooling where SSH is already used. Evaluate the library’s SCP metadata fidelity and progress‑reporting features against your existing workflow, then gradually replace clj‑ssh or direct JSch calls. If you operate in a Babashka‑heavy environment, take advantage of the shim behavior to maintain a single codebase across JVM and script contexts. Keep an eye on the version sync with bbssh to stay aligned with security patches and feature releases, and consult the project’s GitHub issues and documentation for guidance on advanced topics such as connection pooling, keep‑alive tuning, and custom authentication mechanisms. By embracing this modular SSH solution, you gain a reliable, cross‑platform tool that aligns with modern Clojure principles of simplicity, composability, and minimal external dependencies.