In this post I'll give updates about open source I worked on during September and October 2026.
To see previous OSS updates, go here.
I'd like to thank all the sponsors and contributors who make this work possible. Without you, the projects below would not be as mature or would not exist or be maintained at all! So a sincere thank you to everyone who contributes to the sustainability of these projects.

Current top tier sponsors:
Open the details section for more info about sponsoring.
If you want to ensure that the projects I work on are sustainably maintained, you can sponsor this work in the following ways. If you work for a company that uses my OSS, please ask your employer, that would be even better. Thank you!
A lot happened again in the last two months! One major highlight was my visit to the Clojure Conj together with Rahul Dé, where we organized a Babashka workshop. It was great seeing lots of familiar and new faces over there. To give people at the workshop the best experience possible, lots of improvements went into babashka prior to the Conj... workshop driven development I guess. The major milestones I focused on during the last 6-8 months were:
All of this combined made for a great workshop, where we developed a stateless password manager that used cryptography either by shelling out or by FFI. This project combined the most-used libraries in babashka, like babashka.fs, babashka.process, babashka.cli, babashka.ffi, etc. so even if you didn't end up using this password manager (most people probably didn't) you certainly were exposed to what you would typically use in a bb script or project. Rahul and I will give this workshop at EuroClojure 2027 as well, so in case you missed it, you'll get one more chance!


Here are some personal favorite highlights from the last two months.
Antq, a tool to update outdated deps, now runs with bb!
You can now run build.clj in babashka with tools.build (which comes bundled with bb) and get --help and shell completions for your build tasks. Sean Corfield has been adopting this in projects he maintains. See for example: next.jdbc, HoneySQL, logging4j2 and rephrase
(when (and (string? x) (pos? n)) (inc x)) will now warn since (string? x) implies x is a string and (inc x) will crash on that input:unknown-map-key linter that warns when requesting keys from maps that provably aren't there: given (defn opts [] {:host "localhost"}), (:hots (opts)) will warn.Blog posts that I published in this period:
Here are some highlights per project. See each project's CHANGELOG.md for the full list.
Babashka: native, fast-starting Clojure interpreter for scripting.
:deps-resolver :jvm in bb.edn or the add-deps map, or BABASHKA_DEPS_RESOLVER=jvm, to use Java as before. See the blog postclojure.tools.deps and its script namespaces, including clojure.tools.deps.script.make-classpath2, available to scriptsclojure.tools.build.api, with patched install and javac for babashka. A tools.build dependency on the classpath takes precedence and runs from sourcebb repl --connect [addr] to connect to an nREPL server with completion, eldoc, documentation lookup and Ctrl-C interruption. Accepts host:port, a port or unix://path. Reads .nrepl-port by default.127.0.0.1 by default instead of 0.0.0.0. Use bb --nrepl-server 0.0.0.0:1667 in DockerC-c C-t) and cider-versioninterrupt op. Interruption is cooperative and does not stop tight loops#dbg, #break, C-u C-M-x and the step commandsbabashka.deps.maven with find-versions, with-repository-credentials, active-proxy and model-reposbabashka.ffi: call C functions in shared libraries from Clojure.
babashka.ffi, available on Clojars for JVM Clojure, ClojureScript and nbbnode:fficallback accepts struct layouts as argument and return typesbabashka.esbuild: use esbuild from babashka and the JVM
deps-deploy: deploy jars to Clojars and Maven Central from babashka
babashka.http-clientSCI: Configurable Clojure/Script interpreter suitable for scripting
:req!, :excess and :missing directives, :defaults removed. Adds the selector macro to clojure.core.binding (about 4x on the JVM, 2x in native bb) and try without a catch (about 4x)ns-map is 6x faster and ns-refers 60xinstance? on a protocol returning true for a type the protocol is extended to, such as nil or Object, on the JVMclj-kondo: static analyzer and linter for Clojure code that sparks joy.
:unknown-map-key: warn on a keyword lookup of a key not present in a map. E.g. (:hots (opts)) will warn when opts returns {:host "localhost"}.if-not, cond and and. E.g. (when (and (string? x) (pos? n)) (inc x)) will warn.merge and merge-with require a map or nil as first map argument. E.g. (merge [1 2] [3 4]) will warn.:warn-on-reflection also warns on Java constructor calls. E.g. (Date. x) will warn.:clj-kondo/macroexpand-hook: referred macros, helper namespaces, requires without alias and nested hooks (@imrekoszo):order-by, :having and :timeout in Datalevin and Datahike queriesSquint: CLJS syntax to JS compiler
for await in doseq, with ^:await on a binding, (doseq [^:await x xs] ...), or on the binding vector for every binding (#1053, @shaunlebron)eduction, implements? and keyword-identical?or, and and reifyeven? and string? in if, and, or and not#html: escape attribute values from {:& m} (@mskyan)neil: A CLI to add common aliases and features to deps.edn-based projects
neil dep --help no longer throwsbabashka.neil/-main easier to call from code (@lread)neil dep upgrade LIB upgrades only LIB instead of all depsneil dep add, dep upgrade and dep versions find versions in the :mvn/repos of deps.edn. dep versions no longer lists snapshotsbabashka CLI: Turn Clojure functions into CLIs!
:fn and :exec-fn vars in babashka and ClojureDartdispatch: add :supplied to the opts map's :org.babashka/cli metadata with the set of option keys given on the command line:prog is not specified (@lread)nbb: Scripting in Clojure on Node.js using SCI
babashka.ffi built-in library for calling C functions. Requires Node.js 26.1 or newer1.6.44Edamame: configurable EDN and Clojure parser with location metadata and more
deps.clj: a faithful port of the Clojure CLI Bash script to Clojure
--version, --help, -Sdescribe and a cached -Spath to run without Java.*make-classpath-fn* to allow in-process classpath resolution.rewrite-edn: Utility lib on top of rewrite-clj
http-client: HTTP client for Clojure and babashka
uri-with-query no longer decodes the existing query, fragment and user info when :query-params are used (@dylanpulver)decode-body passes a nil body through, e.g. of a 407 to CONNECTfs: file system utility library for Clojure
html: HTML generation library inspired by squint's HTML tag
nil, or false in html mode: [:input {:disabled false}] rendered disabled="false", which disables the input. Matches hiccup (#19, @telekid)true and false as "true" and "false" for aria-*, data-*, contenteditable, draggable, focusable and spellcheck. Names match in any case, so :spellCheck works as wellquickblog: lightweight static blog engine for Clojure and babashka
gh-release-artifact: Upload artifacts to GitHub releases idempotently
Buzz: write a web application with the JVM or babashka only
observe, atom-source and buzz.source/Source API. :watch and invalidate! are gone (#20)raylib-bb-ffi: raylib bindings for babashka via babashka.ffi
babashka.fficljbang: a Clojure-like language that runs as Emacs Lisp
Cherry: Experimental ClojureScript to ES6 module compiler
Cream: Clojure + GraalVM Crema native binary
lein-clj-kondo and clj-kondo-bb: released alongside each clj-kondo release
Contributions to third party projects:
h/expr against a served squint core, tree-shaken with esbuild. See the blog post. Serve source maps for defc components behind :source-map?:link rendererThese are some other projects I'm involved with, but little to no activity happened in the past two months.
Published: 2026-10-30
Tagged: clojure oss updates
Hyper is a server-rendered web framework for Clojure. You can write pages using hiccup, and hyper renders them on the server to HTML. Incremental updates to the HTML reach the browser through server-sent events (SSE), using Datastar.
Here is a basic counter example:
(defn home-page [req]
(let [count* (h/tab-cursor :count 0)]
[:div
[:h1 "Count: " @count*]
[:button {:data-on:click (h/action (swap! count* inc))}
"Increment"]]))
Some interactions can be handled directly in the browser. A hint below a search field, for example, can update as you type without a round trip to the server. Datastar stores client state in so called signals and evaluates JavaScript expressions in data-* attributes. With hyper's h/expr macro, you can write those expressions in Clojure syntax:
(let [query* (h/signal :query "")]
[:div
[:input {:data-bind query*}]
[:span {:data-text (h/expr (if (zero? (.-length @query*))
"Type to search"
(str "Searching for " (subs @query* 0 20))))}]])
The :data-bind attribute binds the input to the query signal. Inside h/expr, @query* compiles to Datastar's $query expression. During macro expansion, Squint compiles the expression to JavaScript:
((($query.length === 0)) ? ("Type to search") : (`${"Searching for "}${hyper_sc.subs($query, 0, 20)}`))
The compiled expression calls subs from squint's core library, which hyper loads as window.hyper_sc. The full library is about 128 KB, or 33 KB gzipped. With esbuild, hyper can remove unused core functions through tree shaking and serve a smaller bundle. Although esbuild is written in Go, it is usually invoked through its npm package. For hyper, it would be convenient to call it directly from Clojure.
Enter babashka.esbuild! This library calls esbuild as a shared library through babashka.ffi. It works on the JVM and in babashka:
{:deps {io.github.squint-cljs/squint {:mvn/version "0.14.210"}
org.babashka/esbuild {:mvn/version "0.1.1"}}}
Here is how to compile a squint function to a JavaScript module and bundle it with esbuild:
(require '[squint.compiler :as squint]
'[babashka.esbuild :as esbuild]
'[clojure.java.io :as io])
(spit "core.js" (slurp (io/resource "squint/core.js")))
(spit "main.js"
(squint/compile-string
"(defn search-hint [query]
(if (zero? (.-length query))
\"Type to search\"
(str \"Searching for \" (subs query 0 20))))"
{:import-maps {"squint-cljs/core.js" "./core.js"}}))
(-> (esbuild/build {:entry-points ["main.js"]
:bundle true
:format :esm
:minify true})
:outputs first :contents)
The compiled main.js looks like this:
import * as squint_core from './core.js';
var search_hint = function (query) {
if ((query.length === 0)) {
return "Type to search"} else {
return `${"Searching for "}${squint_core.subs(query, 0, 20)}`};
};
export { search_hint }
After bundling and minification, esbuild returns:
function e(t,n,r){return t.substring(n,r)}var u=function(t){return t.length===0?"Type to search":`Searching for ${e(t,0,20)}`};export{u as search_hint};
The bundle contains just subs and the search-hint function: 152 bytes in total (145 gzipped). The full core library alone was 128,117 bytes (32,650 gzipped).
In hyper, the compiled h/expr expressions end up in HTML attributes. To find out which core functions these expressions need, hyper uses information returned by the squint compiler. Since version 0.14.210, squint includes the names of the core functions used during compilation:
(:used-core-vars (squint/compile* "(str \"Searching for \" (subs query 0 20))"))
;;=> #{"subs"}
Hyper collects these names from every h/expr and every defc component in the application. At startup (when using the :tree-shake? option), it passes esbuild an entry module that re-exports those functions:
export { subs } from './core.js';
Hyper serves the resulting bundle at /hyper/squint-core.js and exposes it as window.hyper_sc. The defc components use the same object:
const $sc = window.hyper_sc;
The URL's v parameter is a hash of core.js and the names of the core functions used by the application. The bundle stays the same for a given hash, so hyper serves it with a one-year cache lifetime:
/hyper/squint-core.js?v=3f9a1c2b7d4e
Cache-Control: public, max-age=31536000, immutable
When a deployment changes the set of core functions in use, the hash changes. The page records its current version in a data attribute:
<div id="hyper-app" data-hyper-squint-version="1a2b3c4d">
If a server update contains a different version, the page reloads to fetch the new bundle.
To enable tree shaking in hyper, add the esbuild dependency and enable native access for the JVM:
{:deps {org.babashka/esbuild {:mvn/version "0.1.1"}}
:aliases {:run {:jvm-opts ["--enable-native-access=ALL-UNNAMED"]}}}
Then pass :tree-shake? true when creating the handler:
(h/create-handler #'routes :tree-shake? true)
Since the dependency is optional, hyper serves squint's full core.js by default.
Note that hyper has h/defc to define webcomponents using Squint too, which also work with tree-shaking, but I left that out of this blog to keep it short and readable.
As you can see, we can write server-side rendered HTML applications and have some fun using ClojureScript in the form of Squint too, while getting very reasonable JS compilation sizes. And we don't have to think about it at all, just deploy to production with :tree-shake? true and done! No build process.
Published: 2026-09-25
Babashka 1.13.222 is the "conj" release! I'll be giving a babashka workshop at Clojure/conj together with Rahul De. Hope to see you there!
Babashka now resolves dependencies without a JVM by default. Here's a demo: We will use an alternative :mvn/local-repo such that we force downloading deps.
deps-example.clj:
(require '[babashka.deps :as deps]
'[babashka.fs :as fs])
(deps/add-deps
{:mvn/local-repo (str (fs/create-temp-dir {:prefix "bb-mvn-"}))
:deps '{medley/medley {:mvn/version "1.4.0"}}})
(require '[medley.core :as medley])
(prn (medley/index-by :id [{:id 1 :name "Ada"}
{:id 2 :name "Grace"}]))
To make sure this example runs without Java, we will remove it from the PATH and set JAVA_HOME to a non-existing directory as well:
# Find bb before clearing PATH.
bb_executable=$(command -v bb)
env PATH= JAVA_HOME=/does-not-exist "$bb_executable" -Sforce deps-example.clj
# {1 {:id 1, :name "Ada"}, 2 {:id 2, :name "Grace"}}
If you encounter a bug in the new native resolver, switch back to the JVM resolver:
export BABASHKA_DEPS_RESOLVER=jvm
bb deps-example.clj
To select the JVM resolver per project, set this in bb.edn:
{:deps-resolver :jvm}
The native resolver is built on top of clojure.tools.deps. You can now use that directly from bb without adding a dependency:
(require '[clojure.tools.deps :as deps]
'[babashka.fs :as fs])
(def basis
(deps/create-basis
{:root nil :user nil :project nil
:extra '{:deps {medley/medley {:mvn/version "1.4.0"}}}}))
(mapv fs/file-name (:classpath-roots basis))
;;=> ["medley-1.4.0.jar" "clojure-1.9.0.jar"
;; "core.specs.alpha-0.1.24.jar" "spec.alpha-0.1.143.jar"]
In this release, we're going one step further. Since tools.build is built on tools.deps and we now have it in bb, it was only a small leap to also add tools.build.
This example packages a src directory with a build.clj running in babashka:
(ns build
(:require [clojure.tools.build.api :as b]))
(def class-dir "target/classes")
(defn jar [_]
(b/copy-dir {:src-dirs ["src"]
:target-dir class-dir})
(b/jar {:class-dir class-dir
:jar-file "target/app.jar"}))
Add a task in bb.edn:
{:paths ["."]
:tasks {jar {:exec-fn build/jar}}}
bb jar
Everything runs in babashka and we did not need to add a dependency on tools.build. When invoking tasks that require Java compilation, of course, a JVM will be necessary still.
To make sure typical build.clj files will run fully in babashka, a bb-compatible variant of the excellent slipset/deps-deploy library was made: babashka/deps-deploy. I consulted with Eric Assum to perhaps make this library bb-compatible, but his reply was: what if people just used yours from now on... oops ;).
{:paths ["."]
:deps {io.github.babashka/deps-deploy {:mvn/version "0.0.1"}}
:tasks {jar {:exec-fn build/jar}
deploy {:exec-fn build/deploy}}}
Add these requires and the deploy function to build.clj. The :org.babashka/cli metadata supplies option parsing, help and completions:
(ns build
(:require [babashka.deps-deploy :as dd]
[clojure.tools.build.api :as b]))
(def lib 'io.github.yourname/app)
(def version [0 1 0])
(def class-dir "target/classes")
(defn deploy
{:org.babashka/cli
{:spec {:bump {:coerce :boolean
:desc "Increment the patch version before deploying"}}}}
[{:keys [bump]}]
(let [version (cond-> version bump (update 2 inc))]
(b/write-pom {:class-dir class-dir
:lib lib
:version (apply format "%s.%s.%s" version)
:basis (b/create-basis {:project {:paths ["src"]}})
:src-dirs ["src"]})
(jar nil)
(dd/deploy {:artifact "target/app.jar"
:pom-file (b/pom-path {:lib lib
:class-dir class-dir})})))
Like with slipset/deps-deploy, set CLOJARS_USERNAME and CLOJARS_PASSWORD to your Clojars username and deploy token.
bb deploy --help
bb deploy
bb deploy --bump
To enable completions in zsh after compinit:
source <(bb org.babashka.cli/completions snippet --shell zsh)
For other shells, check the completions section in babashka.cli's docs. Completions should now show this:
$ bb deploy --<TAB>
--bump -- Increment the patch version before deploying
--help -- Show this help
A few fixes went into babashka to help with task :exec-fns that depend on each other which should now work smoother as well.
Babashka already had nREPL support, but this release extends it to the wider nREPL Clojure ecosystem. As you were used to, you can start a bb nREPL like this:
bb nrepl-server 1667
You can now connect from another terminal and you'll get a nice jline REPL in which you can talk to your nREPL server:
bb repl --connect 1667
Completion, eldoc, documentation lookup and Ctrl-C interruption all should work. Of course you can talk to any nREPL server, including a normal JVM Clojure one.
The built-in nREPL server now runs nREPL 1.7.0, with CIDER inspector and test runner middleware support (the most frequently missing feature in bb nREPL). It now binds to 127.0.0.1 by default instead of 0.0.0.0.
Programmers (or LLM agents) can use the bundled nrepl.core client to evaluate code in a running REPL. Save as eval.clj:
(require '[nrepl.core :as nrepl])
(with-open [conn (nrepl/connect :port 1667)]
(prn (-> (nrepl/client conn 1000)
(nrepl/message {:op "eval"
:code "(reduce + (range 10))"})
nrepl/response-values)))
bb eval.clj
# [45]
Here's an example of custom middleware. Log the code your editor or client evaluates. Save as middleware.clj:
(require '[nrepl.server :as server]
'[nrepl.middleware :refer [set-descriptor!]])
(defn wrap-log-eval [handler]
(fn [{:keys [op code] :as msg}]
(when (= "eval" op)
(spit "nrepl-eval.log" (str code "\n") :append true))
(handler msg)))
(set-descriptor! #'wrap-log-eval
{:requires #{"clone"}
:expects #{"eval"}
:handles {}})
(def nrepl-server
(server/start-server :bind "127.0.0.1"
:port 1667
:handler (server/default-handler #'wrap-log-eval)))
Start the server:
bb middleware.clj
Connect from another terminal:
bb repl --connect 1667
user=> (reduce + (range 10))
45
Inspect the log on the server:
tail -f nrepl-eval.log
# (reduce + (range 10))
Hope you like these additions to babashka:
babashka.deps-deploy which completes the gap of running build.clj fully in bb nowbuild.clj functions by adding them as :exec-fn in bb.edn. You can get enhanced option parsing and completions by adding :org.babashka/cli metadata to your build.clj functions.nrepl.core and related namespaces are now exposed and used in babashka's own nREPL server. You can use it to connect to otherservers as well. Babashka's repl has a new --connect option which uses this through its own jline REPL.Published: 2026-09-14