October 7, 2026

A look at the Clojure CLI REPL

Just before this year's Clojure/conj, the Clojure team released a new Clojure CLI REPL. Per the README:

A REPL for the Clojure CLI featuring multi-line editing with proper indentation, bracket highlighting, structural editing, inline eval, doc lookup for Clojure and Java, a data inspector, configurable prompts, keybindings, and much more.

The repo includes an example configuration and I think it is the first use case for the recently created tools.deps.config which is intended to provide a standardized way for Clojure tooling to manage its configuration, on a per-project or user-level basis. Per the README:

Clojure tools need a well-known place to store user and project configuration. This library defines that place and provides functions for reading the files stored there.

The idea is that each tool's configuration lives at <location>/.cljconf/<lib-ns>/<lib-name>.edn (and is expected to contain a 1-level map with keyword keys). The CLI REPL's configuration therefore lives at .cljconf/org.clojure/clojure-cli.repl/clojure-cli.repl.edn and alongside that there is a data directory at .cljconf/org.clojure/clojure-cli.repl/ which, in this case, contains a small Clojure project containing code that supports the customization of the CLI REPL.

That .cljconf directory can be in your user-level Clojure configuration (for me, that's $HOME/.config/clojure following XDG conventions, but for many people that would be $HOME/.clojure), or it can be in your project directory, alongside other configuration directories.

With all that preamble out of the way, I'm going to walk through how I have the new CLI REPL set up for my development workflow. But first, some history!

My dot-clojure Repository

Back in March, 2018, I decided to keep my Clojure configuration on GitHub in case anyone found it useful. Back then, it was just my deps.edn file containing the aliases I found useful. In December, 2020, I added a dev.clj file which was my first "adaptive REPL startup code" -- see the initial version of it here.

The idea was that you could use a basic alias to "start a REPL" and, depending on what else was on your classpath, the REPL would include that additional tooling. Back then, I was using Cognitect's REBL data browser, now Nubank's Morse, and Reveal, as well as Rebel Readline. Sometimes, I would just fire up a bare REPL, sometimes I would use Rebel Readline's REPL, and sometimes I would use one of the visualization tools.

Over the intervening five and a half years, my setup evolved a lot, as new tools and libraries appeared, and I changed my workflow to adopt them. By May of this year (2026), I was using this, much more sophisticated REPL setup, which uses Portal (instead of REBL or Reveal), CIDER/nREPL, and my Rephrase library to produce more readable error messages. I was still using Rebel Readline.

Enter clojure-cli.repl

The new Clojure CLI REPL is based on nREPL and aims to provide a similar experience to Rebel Readline (it isn't quite there yet but I expect it will surpass it fairly soon), as well as providing an integrated data inspector built into the REPL (based on datafy/nav).

In addition, it is designed to be extremely configurable, with easily customizable keybindings, including Paredit-style structural editing.

You can browse the latest version of my REPL setup but I'm going to dive into my specific configuration and customization of it.

My clojure-cli.repl.edn Configuration

I'll start with my configuration file, and then talk about the code I wrote to customize the behavior further.

 :auto-require [[clojure.repl.deps :refer [add-lib]]
                [dev.uptime :refer [uptime]]]

The :auto-require setting lets you specify namespaces (and symbols) that should be required (and referred) into every namespace you use via the REPL. I like to be able to invoke add-lib to add new dependencies, and I didn't want to have to require/refer it manually as I work. The uptime function is a little utility that tells me how long my REPL has been running, in a friendly format. I'm quite proud of how long many of my REPL sessions actually live!

 :bracket-pairs true

I'm a bit surprised that the default for auto-close (, [, and { isn't enabled by default, but I guess this is subjective.

 :doc-at-cursor "M-d"
 :eval-form-at-cursor "M-e"

Unchanged from the default in the examples provided with clojure-cli.repl.

 :history :project

The default is for all your REPL history to be shared across projects, in the user-level configuration, but I prefer a per-project history.

 :inspect "M-i"
 :keybindings dev.keybindings/install

Unchanged from the default in the examples.

 :middleware [dev.heap/middleware
              dev.timing/middleware

Unchanged from the default in the examples. These are examples of custom middleware that enhance the REPL experience by showing how much heap is currently used and how long each evaluation takes. A very nice touch!

              ^:optional cider.nrepl/cider-middleware
              ^:optional org.corfield.rephrase.nrepl/middleware
              ;; add the following setting to ~/.config/nrepl/nrepl.edn
              ;; {:portal.nrepl/wrap-portal {:all-evals true}}
              ;; that will tap> all REPL evaluations
              ^:optional portal.nrepl/middleware]

These are the three pieces of middleware I use at different times, depending on whether I want a quick, basic REPL or something more feature-rich (for integration with editors, for example). The ^:optional metadata tells the CLI REPL to just ignore the middleware if it is not on the classpath. This provides the same "adaptive REPL startup code" that I used to have in my previous setup, but in a much cleaner and more maintainable way.

The examples configuration has no Paredit keybindings, but here's what I use the most. Currently, the CLI REPL seems to only support single-key bindings, with a single-modifier, so these are only "similar" to the bindings I'm used to in VS Code/Calva:

 :paredit/barf-forward "M-,"
 :paredit/join "M-j"
 :paredit/kill "M-k"
 :paredit/move-to-prev "M-b"
 :paredit/raise "M-r"
 :paredit/slurp-forward "M-."
 :paredit/slurp-forward-fully "M-/"
 :paredit/splice "M-h"
 :paredit/split "M-l"
 :paredit/wrap-list "M-p"
 :paredit/wrap-map "M-c"
 :paredit/wrap-vector "M-s"

The following are unchanged from the examples:

 ;; note: portal middleware overrides the print hook:
 :print-hook dev.hooks/pprint-values
 :prompt dev.full-width-prompt/prompt

And, finally, I like to see all reflection warnings in all the code I load:

 :warn-on-reflection true

My clojure-cli.repl Customizations

The examples provided in the CLI REPL repo contains these files in the data directory (under src/dev, so all the namespaces start with dev.):

full_width_prompt.clj  
heap.clj  
hooks.clj  
keybindings.clj  
timing.clj

The example keybindings.clj binds C-t to insert a UUID into the REPL, which seems a bit random to me, so I changed it to insert tap-> which is new in Clojure 1.13 and I'm already finding very useful.

In the absence of any global initialization code, I've added a top-level form to the examples hooks.clj file, that "installs" some extra goodies I like to use with Portal - see below.

I mentioned my uptime function above - that is in my uptime.clj file. It is auto-required into every namespace, and uptime is referred in for unqualified use.

And, finally, I added a portal.clj file. If Portal is on the classpath, and either clojure.tools.logging and/or my logging4j2 library is on the classpath, the underlying logging function in those libraries is redefined to also tap> a "logging information" data structure that Portal understands, so that all my logging information also appears in Portal. The dev.portal/install! function is currently call from the example hooks.clj file, but I'm hoping the CLI REPL adds some sort of :init-fn configuration option in the future.

Wrapping Up

All of this has allowed me to retire my custom REPL setup, and some of my old aliases, and rely entirely on the new CLI REPL with my enhancements.

If you're on the Clojurians Slack, join the #clojure-cli-repl channel to chat about this new tooling.

Major kudos to Jarrod Taylor, on the Clojure team, for creating this! I look forward to future enhancements (and possibly seeing this integrated into the Clojure CLI itself, at some point).

Tags: clojure open source