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
In this post I'll give updates about open source I worked on during July and August 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!
In the past two months it was summertime in Europe. Due to a couple of heatwaves, it was the perfect time to spend inside and enjoy my new air conditioning, while coding ;-).
The first half of July was mostly spent on improving performance and compatibility of SCI on CLJS. SCI now JIT-compiles interpreted function bodies to JavaScript at runtime, which closes a lot of the gap with compiled ClojureScript: a tight numeric loop went from ~175ms to ~7ms, over 20 times faster than the interpreter. Implementing core protocols on custom types now also works. There's hardly anything you can't do in SCI that you can do in compiled CLJS. I released new versions of scittle and nbb that take full advantage of this.
Also in the middle of July, clj-kondo got a pretty cool enhancement. It infers types of function arguments from how they are used. E.g. when you write (defn foo [x] (inc x)) we can infer that foo is a function that takes a number. I took this principle as far as I could while preventing false positives. Of course, clj-kondo supports the latest Clojure 1.13 destructuring changes too.
In the second half of July I spent significant time on improving babashka tasks with automatic help and completions, backed by babashka.cli. You can read all about that in this blog post: Babashka tasks with automatic help and completions
In August I had the pleasure of giving a talk about Reagami at Func Prog Sweden. In the talk I gave an interactive demo of how to use Reagami in a Squint project through a REPL. I also went into detail on the algorithm that powers the fast DOM diffing. While preparing for the talk, I added SSR to Reagami too. You can view the talk on YouTube:
The last few weeks of August I created babashka.ffi, a new namespace in babashka to call C libraries. See my previous blog post: Babashka 1.13.220 gets FFI. To validate the design I wrote four libraries with it: babashka.sqlite, babashka.duckdb, babashka.postgres and filewatcher. Each one exercised a different corner of the API, along with some examples based on raylib. PacMan is particularly cool:

Right now I'm looking forward to giving a babashka workshop at the Clojure/conj together with Rahul Dé. We're still polishing the workshop material behind the scenes and I'm excited to see how it's turning out. I'm sure it'll be a lot of fun and hope to catch many of you there.
In between all of this, I also worked on squint. It now supports the core protocols, so you can plug in your own collections and use them with core functions. E.g. you can use Immutable.js with Squint. I'm thinking about lightweight immutable persistent data structures for squint, but so far I haven't had much need for them, outside of Advent of Code puzzles.
The above was all about making existing projects better. But I also had a few new creative ideas:
Here are some highlights per project. See each project's CHANGELOG.md for the full list.
Babashka: native, fast-starting Clojure interpreter for scripting.
babashka.ffi: call C functions in shared libraries straight from babashka and JVM Clojure! See the guide--static and --dynamic options override the automatic selection:exec-args can sit directly on a task, not only under :cli, the way (exec ...) already reads it. Before, it was ignored on an :exec-fn or :cmd task:cli spec adds to the runner-level :tasks {:cli {:spec ...}} instead of replacing it. An option from the runner level keeps its coercion and default, and --help lists it under Inherited options:exec-fn runs when another task :depends on it. Before, it did nothing:exec-fn task named in :depends also parse for the CLI task that runs, with their coercion and default. --help lists them under Inherited options:cmd can be a symbol naming a var that holds the command tree, like :cli. Its namespace loads on demand:depends) too--help and shell completions, through the new :exec-fn and :cmd keys. See the blog post! These task keys should be considered experimental and may change in a future version of babashka, depending on feedback from the community:keys!, :syms!, :strs!, & inside a directive, :select, :all and :defaults. Adds req! and some-vals to clojure.coreclojure.core/Inst protocol on records, types and reify, and with extend-protocol and extend-typeproxy of java.io.Writer supports the one-argument write and append, so binding *out* to it works$HOME when the OS does not supply a home directory, e.g. for LDAP users in the static binary:eval and :print options of clojure.main/repl being ignored in the interactive REPL (@jeroenvandijk)babashka.ffi: call C functions in shared libraries from Clojure. New library, also usable from JVM Clojure. See the guide and the examples. The API is experimental
babashka.sqlite: SQLite for babashka through babashka.ffi
with-conn, queries, aggregates, transactions, last-insert-rowid, interrupt, and create-function! for defining a Clojure function callable from SQLbabashka.duckdb: DuckDB for babashka through babashka.ffi
babashka.postgres: PostgreSQL for babashka through babashka.ffi and libpq
connect, close!, with-conn, query, execute!, with-transaction, in-transaction?, cancel!, json, jsonb, version, server-version:read-json and :write-jsonwith-conn hook, CI on three operating systemsfilewatcher: watch files and directories from babashka
babashka.ffi: FSEvents on macOS, inotify on Linux, ReadDirectoryChangesW on Windows, and polling everywherecloseSCI: Configurable Clojure/Script interpreter suitable for scripting
js/Function. This is enabled by default and needs no configuration. When JIT is enabled, loops and numerical computations become much faster (and, in unrestricted contexts, JS interop too)eval is unavailable (e.g. under a Content Security Policy) SCI falls back to the interpreter. Results, error messages and error locations should be identical. And of course, it works under :advanced compilationjs/globalThis.SCI_DISABLE_JIT = true before loading SCI, or in your Google Closure compile settings with :closure-defines {sci.core/disable-jit true}instance? and js globals no longer fall back to the interpreterdeftype, defrecord and reify, and host code calling protocol methods on such instances dispatches into the sci implementations. Works under :advanced compilationdeftype and defrecord fields are JS accessors on the type's prototype: (.-field x) works on instances, (set! (.-field x) v) mutates deftype fields:unrestricted option on init and eval-string: when true, evaluated code may mutate built-in vars and CLJS instance interop skips :classes checks. The option applies only to the context it was passed toenable-unrestricted-access! now throws. Use the :unrestricted option instead. The old function set a process-global flag that leaked into nested contexts:async true in the attr map of defnaset on a primitive array was reflective and 170x slower than aset-doubleloop now report a located stack frame for the loop form instead of a frame without location (all platforms, including babashka)clj-kondo: static analyzer and linter for Clojure code that sparks joy.
(defn f [s] (subs s 1)) (f 42) will warn, since the evidence (subs s 1) tells us that s should be a string.(defn f [{:keys [x]}] (inc x)) (f {:x "foo"}) will warn. A key whose use rejects nil and that has no :or default is required.(defn cfg [] {:port "8080"}) (let [{:keys [port]} (cfg)] (inc port)) will warn.(inc (:y {})) will warn.if or the body of when when it is guarded by a known predicate. E.g. (if (string? x) (inc x) ...) will warn.clojure.core from 23 to 150 vars. E.g. (interleave 1 [2]) and (mod "a" 2) will warn.:constant-condition: warn on a condition whose truthiness is the same on every run. On by default. Replaces :condition-always-true, whose config and ignores still apply to always-true conditions, and takes over the cond catch-all warning from :unreachable-code:keys!, :syms! and :strs! and report them at call sites:select in map destructuring. The bound map's keys are known to the type checker:defaults in map destructuring, error when used without :or:analyze-call hook rewrites a call, clj-kondo checks the arity of the original function but not its parameter types.:discouraged-var: new per-var :positions option (a set or vector of :call and/or :value) to limit the warning to call position or value position. A var passed to a higher-order function such as map counts as :value.:seq-rest: suggest using (next x) over (seq (rest x)). Defaults to :off (@tomdl89)clojure.test.check.clojure-test/defspec#_ before an unmatched reader conditional discards the next form. E.g. [#_#?(:cljs 1) 2] reads as [] in :clj and will warn.comment forms no longer count for :shadowed-var, :unused-private-var and :inline-def.1.11.babashka CLI: Turn Clojure functions into CLIs!
:positional spec marker: positional args get their own Arguments: help section and may not be passed as options:restrict-args: error on positional args not consumed by :args->opts:cmd-aliases on a table entry or tree node gives a command one or more alternative names.:coerce takes the rest of its token as its value, like getopt: -J-Dfoo=bar binds "-Dfoo=bar", -p80 binds 80. Flag letters may precede the valued option in a cluster: with :b a flag and :a valued, -ba x parses as -b -a x:spec options under Inherited options:. The parser always accepted these options, but help did not show themformat-command-help accepts :spec, the dispatch-level spec, so a standalone call shows the same options as dispatchdispatch: the command named on the command line wins over the :exec-args of its ancestors. A value the user typed at an ancestor level still wins over both:enum values for validation, help and completion:doc and :epilog as a vector of lines, joined with newlines:cmd may be a vector of [name command] pairs, preserving command order without :cmd-order(repeat :k))parse-opts* resolves :spec so its :coerce/:collect entries steer parsing like in parse-opts--keep-order, so fish offers options in the order they are emitted, long option before its short alias, rather than sorting short options firstprefix-needed styleSquint: CLJS syntax to JS compiler
squint.immutable. Added a lot of protocols and made sure core functions work properly with themILookup, IAssociative, IMap, ICounted, IKVReduce, ICollection, IEmptyableCollection and IEquiv protocols. get, assoc, contains?, find, dissoc, count, reduce-kv, conj, empty and = dispatch to them on custom types. Plain objects and arrays keep their fast pathsIStack, IIndexed, IVector, IWriter and IPrintWithWriter protocols, write-all, and an ITransientVector -pop! slot; nth, peek, pop, pop!, subvec, vec, vector?, sequential?, set?, map?, seq, = and printing dispatch to custom collection typesequiv, hash, hash-ordered-coll, hash-unordered-coll and the IHash protocol. hash follows equiv: plain mutable objects and arrays hash by referenceIMeta and IWithMeta protocols; meta and with-meta dispatch through them and the internal meta symbol property is goneclojure.set dispatches through the collection protocols: results keep the input's type, membership tests against a protocol set are value-based, and rename-keys/map-invert no longer mutate a recorddefrecord, record? and the IRecord marker protocol. Records store their fields as own string-keyed properties and implement the map-facing protocols, so keyword lookup, keys, seq, assoc, conj and = work through the regular core functions. assoc keeps the record type, dissoc of a basis field gives a plain map, printing gives #TypeName{:a 1}:keys!/:syms!/:strs! for required keys, & inside them for keys required but not bound, :select, :all, :defaults, and :or by key& {:keys [...]} now destructures a map instead of the raw rest args, and a seq destructured as a map is read as kwargsrecur inside try no longer emits an illegal continue:as-alias in ns :require like CLJS: no runtime import, only a compile-time alias so a namespaced keyword such as ::alias/x resolves:require-global and :refer-global to ns, binding globals loaded via a script tag to consts without emitting an import:squint/compile-time opt-in mechanism for macro/compile-time namespaces. See doc/compile-time.mddefmacro is compile-time only: no longer emitted to the runtime module, and :refering a macro no longer emits a runtime import for it, matching CLJS^:dev/after-load + ^:dev/before-load hooks similar to shadow-cljs.indexOf on a lazy seq now uses reference equality like a JS array, not value equality. This diverges from CLJS but keeps = out of any bundle that only builds lazy seqs, shrinking a conj bundle from 3801 to 2215 bytesSymbol.for for protocol method dispatch, so pulling in multiple copies of squint.core (e.g. via http://esm.sh/) does not break protocol dispatchCherry: Experimental ClojureScript to ES6 module compiler
cherry.test with clojure.test-compatible testing API, requirable as cljs.test or clojure.testcherry.test/report is a multimethod dispatching on [*current-reporter* type] like cljs.test, so reporting can be extended with defmethod^:dev/after-load / ^:dev/before-load hot-reload hooks, sharing squint's implementation: import cherry from 'cherry-cljs/vite.js'reify, defmulti/defmethod and the vswap! macro. #'foo emits foo's value, like squintset! and binding work across ESM modules. cljs.core dynamic vars are exported as accessor boxes proxying the real vardefprotocol :extend-via-metadata impls resolve under the fully qualified method symbol, so replicant's mutation-log renderer works: replicant's own test suite passes under cherrydeftype implementing cljs.core protocols such as Inst, IIterable and IAtom: their marker properties were Closure-renamed in the precompiled core and missing from the emitter's core protocol set. The externs list and the set are now generated from cljs.core's protocols (bb gen-externs) and the build fails on driftPROTOCOL_SENTINEL with coexisting CLJS runtimes in the same JS realm{:squint/compile-time true} load only their compile-time part into the macro environment, like squint--help/-h, argument validation and error messages via babashka.cli's dispatch, like squint. Adds watch and nrepl-server commands, shell tab completion, and reads options from cherry.edn instead of squint.ednChoq: a ~5 MB binary running the cherry compiler on embedded QuickJS
url and util, @babashka/fs, and a test runnerBuzz: write a web application with the JVM or babashka only
tube-pod: turn YouTube videos into a private podcast
yt-dlp, writes an RSS feed and serves bothmulti-snake: snake for as many players as show up
Reagami: A minimal zero-deps Reagent-like for Squint and CLJS
reagami.ssr to render hiccup to an HTML string on the JVM, Babashka, Squint and CLJS. See Server-side renderingreagami.core/render (the regular render function) now hydrates a server-rendered page. It adopts the existing DOM instead of clearing the rootnpm create reagami-app my-app to create a Vite project with hot reload and a browser nREPL:on-render now takes a map: (fn [{:keys [node lifecycle state save]}]). Call save with a value to keep it for the next call, and read it back as state. In previous versions, the hook took three arguments and its return value became the statemoveBefore where the browser has it, so a moved subtree keeps its iframe state, animations, focus and selection (#54)value, checked, selected and disabled on a tag with a hyphen as attributes, not as JS properties. A custom element observes attributes, so a property never had any effect. Native elements still handle them as properties:on-rated, now reach the element through addEventListener, because a browser only wires an on* property for standard events<todo-list> custom element, used from Squint, from JavaScript with and without Reagami:on-render nodes and other :on-render improvementscljbang: a Clojure-like language that runs as Emacs Lisp
fn and defn, loop/recur with a tail position check, try/throw/ex-info, case, atoms, syntax quote including nesting, &form and &env in macros, regex and set literals, #_, edn/read-string, slurp and spitel! for calling Emacs Lisp names that are not valid Clojure symbolsnbb: Scripting in Clojure on Node.js using SCI
js/Function. This is enabled by default. This makes loops, numerical computations and JS interop much fasterglob, copy, move, create-dirs, delete-tree, with-temp-dir, path helpers and more) is available via (require '[babashka.fs :as fs]), matching BabashkaILookup, etc) on deftype and defrecorddeftype/defrecord field interop, set! on ^:unsynchronized-mutable fields, add cljs.core type classes like PersistentHashMap, write-all and goog.math.Longprn in nREPLScittle: Execute Clojure(Script) directly from browser script tags via SCI
js/Function. This is enabled by defaultreagent to 1.2.0, re-frame to 1.4.7, replicant to 2026.06.2 and shadow-cljs to 3.4.11squint-inline: write squint functions and inline expressions in a ClojureScript project
assoc, update-in and select-keys work on those without js->clj and clj->js:js-provider :import, and each function's tree-shaken size is recordedEdamame: configurable EDN and Clojure parser with location metadata and more
:refer + rename in :auto-resolve-ns:auto-resolve-ns, qualify syntax-quoted imported classes (e.g. `Date with (:import [java.util Date])) with the full classname:auto-resolve-ns, leave method, constructor and dotted syntax-quoted symbols (`.toString, `Bar., `foo.bar) as-is, matching Clojurefs: file system utility library for Clojure
@babashka/fs npm packagehttp-client: HTTP client for Clojure and babashka
http-server: serve static assets
Content-Range last-pos per RFC 9110, suffix ranges (bytes=-N, previously a 500), clamping last-pos beyond EOF, reading the full range, and a test suite (@slagyr)Cream: Clojure + GraalVM Crema native binary
pom.xml when there is no deps.edn, and recompile Java sources when a dependency changedgraaljs-cherry: a native-image cherry REPL on GraalJS
--small build without itclj-kondo-browser: a static Clojure source browser built from clj-kondo analysis
grasp: Grep Clojure code using clojure.spec regexes
deps.clj: a faithful port of the Clojure CLI Bash script to Clojure
lein-clj-kondo and clj-kondo-bb: released alongside each clj-kondo release
These are some other projects I'm involved with, but little to no activity happened in the past two months.
Published: 2026-09-02
Tagged: clojure oss updates
Today babashka 1.13.220 is released, with a new babashka.ffi namespace for calling C libraries directly from Babashka scripts. The babashka.ffi library is also available as a standalone library for JVM Clojure, so you can use it in your Clojure projects as well. Note that the API is still experimental, although no changes are currently planned. It just needs more exposure and your feedback :). Here's a small demo.
This example loads libz from your system and requests the version.
(require '[babashka.ffi :as ffi :refer [defcfn]])
(def zlib (ffi/load-system-library "z"))
(def zlib-version (ffi/cfn zlib "zlibVersion" [] :string))
(zlib-version)
;;=> "1.3.1"
This example loads an OS-specific library for doing math:
(ffi/load-library
{:mac "libm.dylib"
:linux "libm.so.6"
:windows "ucrtbase.dll"})
(defcfn cos "cos" [:double] :double)
(defcfn pow "pow" [:double :double] :double)
(cos 0.0) ;;=> 1.0
(pow 2.0 10.0) ;;=> 1024.0
To get a feeling for how to use it in larger, non-trivial projects, read the library guide. Some of the API decisions like defcfn are clearly inspired by coffi, so I want to thank Joshua Suskalo for leading the way with his excellent library. But babashka.ffi is not simply a copy of coffi. It does a few things differently. You can provide an explicit library (or a function or delay that resolves to one) to defcfn for example. Also it has a place concept (inspired by Specter's paths) that efficiently lets you read from and write to structs and unions. Like coffi, babashka.ffi builds on java.lang.foreign and makes you manage memory explicitly through arenas. One benefit of this is that you'll get exceptions rather than segfaults that tear down your REPL and you can use with-open to release allocated memory.
To use babashka.ffi and libraries that build on it, you have to use a dynamically linked version of babashka. On Mac and Windows this was always the default. On Linux, the static binary was preferred historically since it did not depend on your system's libc and zlib. In this release we flip this default to a mostly-static binary: all the shared C libraries that babashka needs are statically linked, and glibc is the only dynamically linked part. The aarch64 binary, although it carries -static in its name, was already built this way. Babashka on Linux is built in a container that pins the glibc version to the lowest one possible so it should work on all mainstream LTS versions of Linux today. If you still prefer the fully static binary, you can use the install script with the --static flag. If you use a package manager or a GitHub Action to install babashka, it may not yet be up to date with this new policy. If that is the case, feel free to open an issue at the babashka GitHub repo and I'll reach out to get this fixed. Meanwhile you can install babashka using the installer script on GitHub to a temporary directory to get a second installation of babashka with FFI enabled:
$ curl -sLO https://raw.githubusercontent.com/babashka/babashka/master/install
$ bash install --dir /tmp/bb-test
$ /tmp/bb-test/bb -e "(require '[babashka.ffi :as ffi]) (ffi/load-system-library \"z\")"
The installer script probes your system for the supported glibc version and falls back to the fully static version when necessary.
To validate the design of babashka.ffi, I built a few shiny demos:

A one-liner to try these demos:
$ bb -e '(load-string (slurp "https://raw.githubusercontent.com/babashka/ffi/main/examples/pacman.clj"))'
To validate the design of babashka.ffi even more, a couple of new libraries were born. These libraries mostly resemble existing pods but now use FFI to fulfill similar use cases.
One cool thing you could not do with a pod before is defining a Clojure function in SQLite:
(require '[babashka.sqlite :as sq])
(sq/with-conn [db nil]
(sq/create-function! db "initials"
(fn [s] (apply str (map first (clojure.string/split s #" ")))))
(sq/query db ["select initials(?) i" "gerald jay sussman"]))
;;=> [{:i "gjs"}]
This release also has some really nice task improvements: :exec-fn tasks now compose through :depends. A task can depend on another CLI task, and the dependency's options parse, coerce and show up in --help and shell completion:
{:tasks
{compile {:exec-fn build/compile-sources
:cli {:spec {:release {:coerce :boolean}}}}
jar {:depends [compile]
:exec-fn build/jar}}}
$ bb jar --help
...
Inherited options:
--release
Also you can now directly provide :exec-args on a task:
{:tasks
{deploy {:exec-fn deploy/run
:exec-args {:env "staging"}}}}
A :cmd tree can now be provided through a var, whose namespace is loaded on demand:
{:tasks
{cli {:cmd my.project.cli/commands}}}
While developing FFI and while validating the design through examples and writing libraries, I have made use of LLM assistance.
Hope you'll like these new features!
The full changelog can be found here.
Published: 2026-08-31