Announcing ClojureSSH

A Clojure SSH library thats API compatible with bbssh

Crispin Wellington
12 June 2026 ยท 5 min read
unsplash-logoDerek Thomson

clojuressh: Unified SSH Support for Clojure and Babashka

A while back I wrote about my plans for Spire saying that the codebase needed to be broken into smaller, more focused libraries. The first of those was bbssh. The second is clojuressh. It is a Clojure library wrapping JSch, extracted and cleaned up from the SSH support that's been inside Spire for years, and made API compatible with bbssh.

clojuressh gives you SSH from Clojure without requiring a local ssh binary. It covers:

  • Remote command execution
  • SCP file transfer
  • Port forwarding
  • Proxy support
  • SSH agent integration
  • SSH authentication support
  • Session and channel management
  • Host key and known hosts handling
  • Key pair operations (load, generate, decrypt, write)
  • SSH config file parsing
  • Terminal utilities (raw mode, terminal dimensions, readline)
  • Custom user-info callbacks for interactive prompts
  • Integration with babashka.process

API Compatibility with bbssh

clojuressh was designed to share its API with bbssh. The namespaces and function signatures are identical across both. The version numbers match for compatibility. So clojuressh 0.7.0 is the first release and matches the API of bbssh 0.7.0. This version sync will continue with dual releases going forwards.

Using it in Babashka Instead of bbssh

One interesting facet of clojuressh is that it can be used directly as a dependency inside babashka. In this case it acts as a shim to the bbssh pod. This means you can add it as a dep in your deps.edn and use it the same way in both Clojure and Babashka. There is no need for reader conditionals or pod-loading boilerplate.

;; deps.edn - works for both `clj` and `bb`
{:paths ["src"]
 :deps {org.clojure/clojure {:mvn/version "1.12.5"}
        io.epiccastle/clojuressh {:mvn/version "0.7.0"}}}
(ns myapp.core
  (:require [clojuressh.core :as ssh]
            [clojuressh.session :as session])
            [clojuressh.terminal :as terminal]))

(defn -main []
  (terminal/get-width) ;; print JNA warning early on JDK 22+
  (let [s (ssh/ssh "my-host")]
    (-> (ssh/exec s "uname -a" {:out :string})
        deref
        :out
        println)
    (session/disconnect s)))

Run with Clojure:

clj -M -m myapp.core

Run with babashka:

bb --config deps.edn -m myapp.core

Differences from clj-ssh

If you are just writing code for clojure and don't care about Babashka why not just use clj-ssh? Here's a few things clojuressh does differently:

SCP. I originally used clj-ssh in Spire but ran into limitations with its SCP implementation. The SCP code in clojuressh comes from Spire where it has had a lot of real-world use. clj-ssh fails to preserve timestamps or file permissions on files that are copied. clojuressh also supports a progress-fn that can be passed a callback and used to monitor the progress of the copy making it easy to implement things like user feedback.

Default user interactions. clojuressh integrates with your existing keychain. In the case of presented keys that need decrypting or passwords that need entering, clojuressh falls back to behaviour just like ssh, prompting the user. The same is true for unknown host keys, asking the user if you want to trust the unknown host key and then adding it to known hosts.

SSH config file parsing. clojuressh.config-repository can read ~/.ssh/config, picking up your host aliases, port overrides, and identity file settings.

Terminal management. The clojuressh.terminal namespace handles raw mode, terminal size, and readline which can be useful if you're building anything interactive.

Inter-operation with babashka.process. clojuressh.core/exec supports process threading with -> to and from babashka.process processes. This means you can stream stdout from a local shell command to a remote command and back to a local process again with a single ->:

(-> (babashka.process/process "echo this is local")
    (clojuressh.core/exec "md5sum" {:session session})
    (babashka.process/process
        "bash -c \"echo 'our sum: $(cat)'\""
        {:out :string})
    deref
    :out)
;; => "our sum: 4088b54321c3a731eda432ab09fa9f63 -\n"

Note on JDK 22+: clojuressh uses JNA for terminal handling. On JDK 22 and newer the JVM will print a warning the first time a native library loads. This isn't an error. The README covers how to silence it with --enable-native-access=ALL-UNNAMED or to print it early. It's confusing because it usually pops up just after clojuressh prompts for the user password.

Get your hands on it

Use the following coordinates:

;; tools.deps
io.epiccastle/clojuressh {:mvn/version "0.7.0"}

;; Leiningen
[io.epiccastle/clojuressh "0.7.0"]

Find the github project here or read the documentation here.

About Crispin Wellington

Crispin Wellington is a Software Developer and System Operations expert with over 25 years of professional experience. As well as being the founder of Epic Castle he is the founder and developer of backgammonbuddy.com. He is based in Perth, Australia and is a husband and father.