REPL-Based Development

This section explores REPL-based development in Clojure and how it enhances agentic engineering workflows.

What is the REPL?

REPL stands for read, eval, print, and loop. It is a interactive shell that allows you to evaluate Clojure expressions and have those Clojure expressions be printed.

It is the defacto method for exploring and interacting with Clojure code. It turns out that a full featured repl is excellent tool for working with large language models and preventing AI hallucinations.

Starting the REPL

In this manual we’re going to use the clj-mcp-light tool. First first we’re going to need a nREPL process in order to interact with closure code. Here is a very basic setup that you can use in any Clojure project.

{:deps {org.clojure/clojure {:mvn/version "1.12.4"}
        io.github.tonsky/clj-reload {:mvn/version "1.0.0"}}
 :aliases {:nrepl
           {:extra-deps
            {nrepl/nrepl {:mvn/version "1.3.0"}
             cider/cider-nrepl {:mvn/version "0.56.0"}}
            :jvm-opts ["-Djdk.attach.allowAttachSelf"]
            :main-opts ["-m" "nrepl.cmdline"
                        "--middleware" "[cider.nrepl/cider-middleware]"
                        "--port" "7889"]}}}

This pull the Clojure nRepl dependency and sets up the nRepl to always start on the 7889 port.

Lets now start the repl process and try to connect to it.

clojure -M:nrepl
clj-nrepl-eval -p 7889 "*ns*"

Reload

In order to be effective with the REPL, we need a mechanism for loading and reloading Clojure code without having to restart the Clojure process. The clj-reload library is an excellent tool that does this. The clojure.tools.namespace project also has similar functionaity.

(ns dev
  (:require [clj-reload.core :as reload]
            [clojure.repl :as repl))

 (reload/init
  {:dirs ["src" "dev" "test"]})

  (println (repl/doc reload/reload))

Note the that print the doc string for the reload function. This prints the docs for that function every time the namespace is loaded. This is helpful to remind the LM agent about these tools.

Interactive Development

With the nREPL running and clj-nrepl-eval connected, the same REPL workflow that Clojure developers already use becomes a grounding mechanism for LLM agents. The agent can evaluate expressions, call clojure.repl/doc, inspect data, and test functions against a live runtime – all through standard bash commands.

The key insight is that every REPL evaluation gives the agent a fact from the running system instead of a guess from training data. An agent that calls (clojure.repl/doc clojure.set/intersection) before using it will not hallucinate the argument order. An agent that evaluates (my-function nil) before saving will discover the nil handling bug immediately.

The sections below describe how to structure this workflow to get the most out of it.

The REPL as Hallucination Prevention

Without a REPL, an LLM agent writes code based entirely on its training data and the current context window. It has no way to check whether a function exists, what arguments it takes, or how it behaves on edge cases. The result is code that looks plausible but may be wrong in subtle ways – a function called with its arguments in the wrong order, a missing initial value to reduce, a method that does not exist in the version of the library you are using.

The REPL solves this by giving the agent access to the actual running system. When the agent can evaluate (clojure.repl/doc some-fn) and get back real documentation, or call (some-fn test-input) and see the actual result, it is working with facts instead of statistical predictions.

Validation Before Saving

The most impactful discipline is requiring the agent to validate functions in the REPL before writing them to files. Consider a simple example:

clj-nrepl-eval -p 7889 "(defn sum-evens [nums] (->> nums (filter even?) (reduce +)))"
clj-nrepl-eval -p 7889 "(sum-evens [1 2 3 4 5 6])"
# => 12

The happy path works. But what about edge cases?

clj-nrepl-eval -p 7889 "(sum-evens [])"
# => ArityException (reduce needs an init value)

The agent discovers that reduce without an initial value fails on an empty collection. Without the REPL, this bug would have been written to a file and discovered much later. With the REPL, the agent fixes it immediately:

clj-nrepl-eval -p 7889 "(defn sum-evens [nums] (->> nums (filter even?) (reduce + 0)))"
clj-nrepl-eval -p 7889 "(sum-evens [])"
# => 0

This define-test-fix-save loop is the core discipline. When you see your agent skipping REPL validation and writing code directly to files, that is a sign something needs to change in your workflow.

Exploration Before Use

Agents frequently hallucinate function signatures, argument orders, or behavior – especially for less common libraries. Instructing the agent to explore unfamiliar functions before using them eliminates this class of error cheaply. A call to clojure.repl/doc or clojure.repl/source takes milliseconds and replaces a guess with a fact.

The clojure.repl namespace provides the tools for this: doc, dir, apropos, find-doc, and source. An agent that uses these freely before writing code will produce significantly fewer errors than one that relies on what it remembers from training.

Codebase Awareness

A second category of hallucination comes from the agent ignoring the conventions of your codebase. It might use camelCase in a kebab-case project, or duplicate a utility function that already exists in another namespace.

The REPL helps here too. The agent can require a namespace, call clojure.repl/dir on it, and immediately see what functions already exist. It can call existing functions to understand their behavior before deciding whether to modify or extend them.

The Paren Edit Death Loop

LLM agents frequently produce mismatched delimiters when editing Clojure code. A missing closing parenthesis triggers a parse error, the agent attempts to fix it, the fix introduces a new error, and the cycle repeats. Bruce Hauman calls this the “Paren Edit Death Loop.”

The clj-paren-repair tool breaks this cycle. It uses parinfer to automatically repair delimiters based on indentation:

clj-paren-repair path/to/file.clj

Having the agent run clj-paren-repair on delimiter errors instead of attempting manual fixes is one of the places where tooling makes a dramatic difference in agent effectiveness.

Passive Prompting Through the REPL

A useful technique for keeping the agent informed is printing docstrings when the dev namespace loads. As shown in the Reload section above:

(println (repl/doc reload/reload))

Every time the agent reloads the dev namespace, it sees the documentation in the REPL output. This is a form of passive prompting – the agent is reminded of available tools without you having to repeat yourself. You can extend this pattern to any function or library the agent should be aware of.

Best Practices

Keep the REPL Process Running

Restarting the Clojure process is expensive. It requires reloading all dependencies and re-initializing state. More importantly, when the nREPL is down, the agent loses its connection to the running system and falls back to writing code without validation. Treat the nREPL process as infrastructure. Keep it running for the duration of your development session and use clj-reload or clojure.tools.namespace to reload changed code.

Test Fixtures

Test namespaces in Clojure often use fixtures to bind dynamic variables like *db* or *config*. Agents that do not know about fixtures will call test functions directly, bypassing fixture setup and producing confusing unbound variable errors.

The agent needs to use clojure.test/run-test-var or clojure.test/run-tests rather than calling test functions directly:

# Correct -- runs with fixtures
clj-nrepl-eval -p 7889 "(clojure.test/run-test-var #'myapp.test/my-test)"

# Incorrect -- bypasses fixtures
clj-nrepl-eval -p 7889 "(myapp.test/my-test)"

This is a common source of confusion for agents and worth documenting explicitly in whatever instructions you provide to the agent.

Pipeline Debugging

When an agent produces a threading pipeline that gives wrong results, having it evaluate each step individually is an effective debugging technique. Defining intermediate values with def makes it easy to inspect the data at each stage:

clj-nrepl-eval -p 7889 "(def raw-data (fetch-data))"
clj-nrepl-eval -p 7889 "(def filtered (filter valid? raw-data))"
clj-nrepl-eval -p 7889 "(count filtered)"
clj-nrepl-eval -p 7889 "(first filtered)"