A live nREPL wire into your running Spring Boot app. Dev only. You've been warned.
Your AI agent can read your code. What it can't do is ask your app a question.
Livewire fixes that. It embeds a Clojure nREPL server inside a running Spring Boot application — giving an AI agent (or a curious developer) a live, stateful probe into the JVM. Beans, queries, transactions, security context, and all.
;; How many queries does /api/books actually fire?
(trace/detect-n+1
(trace/trace-sql
(lw/run-as "member1"
(.getBooks (lw/bean "bookController")))))
;; => {:total-queries 481, :suspicious-queries [{...} {:count 200} ...]}481 queries. For a list page. Now you know. Now you can fix it — without restarting the app.
Not a Clojure developer? Don't worry about the syntax above — the agent writes and runs it for you. The parentheses look foreign at first, but the language is actually small, consistent, and surprisingly readable once your eyes adjust. If you ever feel curious enough to type a snippet yourself, the basics take an afternoon. You won't regret it. But you don't have to.
Livewire is young. The core ideas are solid and it works well in practice, but the API surface
(Clojure function names and signatures, CLI wrapper scripts, SKILL.md structure) will change
as the tool evolves.
This matters less here than it would in a traditional library — and that's by design.
The real contract between Livewire and an AI agent isn't the Clojure API; it's SKILL.md.
The agent reads the skill file fresh at the start of every session. When the API changes,
SKILL.md changes with it, and the agent adapts automatically — no version pinning, no
migration guide to follow. The surface can evolve without breaking the workflow.
Development itself is heavily AI-assisted, which keeps SKILL.md honest: the same agent
that uses Livewire helps build it, so the documentation reflects how the tool actually
behaves, not how it was originally specced.
What this means in practice:
- Pin a version in your
pom.xml/build.gradleif you need stability - After upgrading, copy the new
skills/livewire/directory from the release (SKILL.mdand thebin/wrapper scripts) — that's all the migration there is - Feedback, bug reports, and ideas are very welcome
Modern Spring Boot development has a fundamental feedback loop problem. AI agents make it worse.
edit → restart (30–120s) → observe → repeat
Agents reason statically. They read the code, form a hypothesis, and apply a fix — but they can't observe the running system. So they guess. And when they're wrong, you restart again.
Livewire breaks the loop:
observe → hypothesise → hot-swap → verify → recompile
(zero restarts ──────────────────────────────)
Recompile and the query-watcher auto-applies your @Query changes live.
No REPL call needed — no restart either.
| Requirement | Notes |
|---|---|
| ☕ Java 17+ | Required |
| 🍃 Spring Boot 3.x or 4.x | Hibernate 6 and 7 both supported |
| 🤖 AI agent | Claude Code, ECA, or any agent with an nREPL tool |
| 🍺 bbin | For installing clj-nrepl-eval |
⚡ clj-nrepl-eval |
CLI wrapper the agent uses to talk to the nREPL (see Connecting below) |
Scope it to your local/dev profile — Livewire should never ship to production.
Maven
<dependency>
<groupId>net.brdloush</groupId>
<artifactId>livewire</artifactId>
<version>0.6.0</version>
<!-- scope to dev — never ship this to production -->
</dependency>Gradle
// developmentOnly or a dev-profile configuration
developmentOnly 'net.brdloush:livewire:0.6.0'Livewire auto-configures itself when two conditions are met:
- The JAR is on the classpath
- The property
livewire.enabled=trueis set
Add it to whichever local properties file your project already uses:
# application-local.properties (or -dev, -sandbox, whatever you call it)
livewire.enabled=true
# Optional: override the default nREPL port
livewire.nrepl.port=7888# application-local.yml
livewire:
enabled: true
# nrepl:
# port: 7888You'll see this in the logs on startup:
[livewire] nREPL server started on port 7888 with user aliases (lw, q, intro, trace, qw, hq, jpa, mvc)
That's it. No annotations, no Spring profiles to configure, no code changes.
M-x cider-connect-clj → localhost → 7888
All 8 namespaces are pre-aliased in the user namespace at startup — no require needed:
| Alias | Namespace |
|---|---|
lw |
core — beans, transactions, run-as, properties |
q |
query — raw SQL, diff-entity |
intro |
introspect — endpoints, entities, schema |
trace |
trace — SQL tracing, N+1 detection |
qw |
query-watcher — auto-apply @Query on recompile |
hq |
hot-queries — live @Query swap + restore |
jpa |
jpa-query — JPQL via live EntityManager |
mvc |
mvc — response serialization |
Just connect and start typing — (lw/info) is a good smoke-test.
clojure -Sdeps '{:deps {nrepl/nrepl {:mvn/version "1.3.1"}}}' \
-M -m nrepl.cmdline --connect --host 127.0.0.1 --port 7888For AI agent use, you'll need clj-nrepl-eval — a tiny CLI tool the agent uses to
evaluate Clojure expressions against the live nREPL. Install it via
bbin:
bbin install https://github.com/bhauman/clojure-mcp-light.git \
--tag v0.2.1 \
--as clj-nrepl-eval \
--main-opts '["-m" "clojure-mcp-light.nrepl-eval"]'Then point your agent at port 7888 and load the Livewire skill (see the SKILL.md section
below). All 8 namespaces are pre-aliased at startup — no manual require needed.
Start every session with lw-start — it discovers the nREPL, prints app info, and confirms
the connection is live:
lw-start
# [livewire] connected to localhost:7888
# {:application-name "my-app", :spring-boot-version "4.0.1",
# :hibernate-version "7.2.0.Final", :java-version "21"};; What repos are registered?
(lw/find-beans-matching ".*Repository.*")
;; => ("bookRepository" "authorRepository" "reviewRepository" ...)
;; All registered bean names
(lw/bean-names)
;; => ("bookRepository" "authorRepository" ... "dataSource" ...)
;; All beans of a given type
(lw/beans-of-type javax.sql.DataSource)
;; => [{:name "dataSource", :bean #object[HikariDataSource ...]}]
;; What DB URL is the app actually talking to?
(lw/props-matching "spring\\.datasource\\.url")
;; => {"spring.datasource.url" "jdbc:postgresql://localhost:32808/test"}
;; Runtime environment summary
(lw/info)
;; => {:spring-boot "4.0.1", :spring "7.0.2", :hibernate "7.2.0.Final", :java "25", ...};; Raw SQL through the live DataSource — always cap results
(lw/in-readonly-tx
(q/sql "SELECT id, title FROM book LIMIT 5"))
;; => [{:id 1, :title "All the King's Men"} ...]
;; Repository calls — always page, never call .findAll without a Pageable
(lw/in-readonly-tx
(->> (.findAll (lw/bean "bookRepository")
(org.springframework.data.domain.PageRequest/of 0 3))
.getContent
(mapv #(select-keys (clojure.core/bean %) [:id :title :isbn]))))
;; => [{:id 1, :title "All the King's Men", :isbn "979-0-925405-37-0"} ...]
;; Mutations roll back automatically — safe to experiment
(lw/in-tx
(.save (lw/bean "bookRepository") ...)
(.count (lw/bean "bookRepository")))
;; => 201 (and then silently rolled back)Spring Security doesn't know about your REPL. Without a SecurityContext it'll throw
AuthenticationCredentialsNotFoundException the moment you call anything @PreAuthorize-guarded.
run-as sets one for the duration of the call:
;; ✅ Preferred: vector form — [username role1 role2 ...]
(lw/run-as ["repl-user" "ROLE_MEMBER"]
(.getBookById (lw/bean "bookController") 25))
;; Multiple roles
(lw/run-as ["repl-user" "ROLE_ADMIN" "ROLE_MEMBER"]
(.getStats (lw/bean "adminController")))
;; ⚠️ Plain string form — only grants ROLE_USER + ROLE_ADMIN
;; Will throw AuthorizationDeniedException for MEMBER/VIEWER-gated endpoints
(lw/run-as "admin"
(.getBookById (lw/bean "bookController") 25));; See every SQL a call fires — wrap it and look
(trace/trace-sql
(lw/in-readonly-tx
(.count (lw/bean "bookRepository"))))
;; => {:result 200, :count 1, :duration-ms 8,
;; :queries [{:sql "select count(*) from book b1_0", :caller "..."}]}
;; Detect N+1 automatically
(trace/detect-n+1
(trace/trace-sql
(lw/run-as "member1"
(.getBooks (lw/bean "bookController")))))
;; => {:total-queries 481,
;; :suspicious-queries [{:sql "select ... from book_genre ...", :count 200}
;; {:sql "select ... from review ...", :count 200}
;; {:sql "select ... from library_member", :count 50}
;; {:sql "select ... from author ...", :count 30}]}
;; For @Async / CompletableFuture — capture SQL across all threads
(trace/trace-sql-global
(lw/run-as "member1"
(.getBooksByGenreAsync (lw/bean "bookService") 1)))
;; => {:result [...], :count 12, :queries [...]}481 queries for one endpoint. Four N+1 suspects flagged automatically. Now let's fix it.
No restart needed. Swap the JPQL, verify with trace-sql, iterate, commit the fix:
;; See what @Query methods exist on a repo
(hq/list-queries "bookRepository")
;; => ({:method "findAllWithAuthorAndGenres",
;; :jpql "SELECT DISTINCT b FROM Book b JOIN FETCH b.author LEFT JOIN FETCH b.genres"}
;; {:method "findByGenreId", ...} ...)
;; Swap to a candidate fix
(hq/hot-swap-query! "bookRepository" "findAllWithAuthorAndGenres"
"SELECT DISTINCT b FROM Book b JOIN FETCH b.author
LEFT JOIN FETCH b.genres LEFT JOIN FETCH b.reviews")
;; Verify — does the query count drop?
(trace/trace-sql
(lw/run-as "member1"
(.getBooks (lw/bean "bookController"))))
;; Restore when done — don't leave swapped queries hanging
(hq/reset-all!)
;; => [["bookRepository" "findAllWithAuthorAndGenres"]]Alternatively: just edit the @Query in your IDE, recompile, and the query-watcher
picks it up automatically — same result, no REPL call:
;; Check watcher status
(qw/status)
;; => {:running? true, :disk-state-size 8}
;; After recompiling in your IDE — watcher fires automatically:
;; [query-watcher] detected change: bookRepository#findAllWithAuthorAndGenres
;; [hot-queries] watcher re-swapped ✓;; All HTTP endpoints with their auth requirements
(->> (intro/list-endpoints)
(filter #(re-find #"books" (str (:paths %))))
(mapv #(select-keys % [:paths :methods :handler-method :pre-authorize])))
;; => [{:paths ["/api/books"], :methods ["GET"],
;; :handler-method "getBooks", :pre-authorize "hasRole('MEMBER')"}]
;; All Hibernate-managed entities
(map :name (intro/list-entities))
;; => ("Author" "Book" "Genre" "LoanRecord" "LibraryMember" "Review")
;; Entity schema for one entity — straight from Hibernate's live metamodel
(intro/inspect-entity "Book")
;; => {:table-name "book",
;; :identifier {:name "id", :columns ["id"], :type "long"},
;; :properties [{:name "title", :columns ["title"], :type "string", :is-association false} ...]}
;; Full domain model in one call — great for ER diagrams
(intro/inspect-all-entities)
;; => {"Book" {:table-name "book", :identifier {...}, :properties [...]}
;; "Author" {:table-name "author", ...}
;; ...};; Execute any JPQL via the live EntityManager — returns plain Clojure maps
(jpa/jpa-query "SELECT b FROM Book b ORDER BY b.id" :page 0 :page-size 5)
;; => [{:id 1, :title "All the King's Men", :author {:id 6, ...}, :genres "<lazy>"} ...]
;; Scalar projections with AS aliases become named keys
(jpa/jpa-query
"SELECT b.title AS title, COUNT(lr) AS loans
FROM Book b JOIN b.loanRecords lr
GROUP BY b.id, b.title ORDER BY COUNT(lr) DESC"
:page-size 5)
;; => [{:title "Vanity Fair", :loans 7} ...]Lazy collections render as "<lazy>" rather than firing surprise queries.
Paged by default (:page-size 20).
;; Invoke a controller method under a live SecurityContext
;; and serialize with the exact same Jackson ObjectMapper Spring MVC uses
(mvc/serialize
(lw/run-as ["repl-user" "ROLE_MEMBER"]
(.getBooks (lw/bean "bookController")))
:limit 3)
;; => ^{:total 200, :returned 3, :content-size 51529, :content-size-gzip 8299}
;; [{"id" 1, "title" "All the King's Men", ...} ...]Or use the CLI wrapper (runs traced, returns JSON + metadata):
lw-call-endpoint bookController getBooks ROLE_MEMBER
lw-call-endpoint --limit 5 adminController getMostLoaned ROLE_ADMINNo database changes — the thunk runs inside a transaction that always rolls back. The diff shows exactly which fields changed and their old/new values:
(q/diff-entity "Book" 42
(fn [] (.archiveBook (lw/bean "bookService") 42)))
;; => {:before {:archived false, :archivedAt nil, :availableCopies 3}
;; :after {:archived true, :archivedAt "2026-03-15T23:49", :availableCopies 3}
;; :changed {:archived [false true]
;; :archivedAt [nil "2026-03-15T23:49"]}}Useful for discovering unintended writes, verifying a fix touched exactly the right fields, or systematically calling suspect service methods until the guilty one confesses.
Livewire is a dev-only tool and is intentionally not subtle about it.
The nREPL can query any table, call any service, and access anything the JVM can touch. This is the point — and the risk. Never enable Livewire against a production database or any environment with real user data.
Use it with:
- A local development database seeded with anonymized or synthetic data
- A sandbox / staging environment that is completely isolated from production
- Testcontainers-spun databases (like the Bloated Shelf playground below)
- A self-hosted LLM — your ground, your rules, no data leaving the building
There is no sandbox. Connecting to port 7888 means executing arbitrary JVM code. Exposing this port outside localhost is equivalent to handing over the JVM process.
# ✅ default — localhost only
livewire.nrepl.bind=127.0.0.1
# ❌ please don't
livewire.nrepl.bind=0.0.0.0Livewire defaults to 127.0.0.1 and will not bind to a broader interface unless
you explicitly tell it to. That's a guardrail, not a permission slip.
Livewire is provided as-is under the MIT license. The authors accept no liability for misuse, data exposure, or any damage resulting from use outside its intended scope.
Bloated Shelf is a real Spring Boot app — Spring Security, JPA, PostgreSQL, multiple roles, a handful of controllers and services, a domain model with real relationships. It happens to have an N+1 problem baked in, but that's just one reason to visit.
The real reason: it's a safe, self-contained Spring app you can hand to an AI agent along with a live Livewire nREPL and just... see what happens.
You will be surprised. Give an agent live, responsive tools with a fast feedback loop and it stops guessing. It starts exploring. It asks questions. It forms hypotheses, tests them in seconds, and builds on what it learns. The creativity that comes out of a well-equipped agent with shiny new toys is something you have to see to believe.
30 authors · 200 books · 50 members · ~5 reviews/book · all lazily loaded
git clone https://github.com/brdloush/bloated-shelf
cd bloated-shelf
mvn spring-boot:run -Dspring-boot.run.profiles=dev,seedTestcontainers spins up a PostgreSQL 16 container automatically. No external database needed. The Livewire nREPL comes up on port 7888.
- 🔎 Hunt the N+1: call the
bookControllerand watchtrace-sqlreport 481 queries — then fix it without restarting - 🧭 Discover the app cold: ask an agent to map out the domain model, endpoints, and auth rules using only the live REPL — no source reading
- 🔥 Hot-swap queries: iterate on JPQL live, measure each variant with
trace-sql, find the winner, commit - 🔒 Test auth boundaries: call the same endpoint under different roles with
lw/run-as, see what changes - 📊 Profile and compare: trace the naive N+1 endpoints against the clean aggregation queries in
adminController - 🧪 Prototype in Clojure, ship in Java: re-implement a service method as a REPL expression, validate query count, then write the real fix
- 💬 Ask nontrivial questions about your data: "Which genre has the most overdue loans?", "Who are the top reviewers and what do they have in common?" — the agent will introspect the entity model, figure out the schema, iterate on queries, and come back with an actual answer. Powerful BI in an agentic chat, no dashboard required
- 🤖 Let the agent loose: point a capable agent at the nREPL, give it
SKILL.mdas context, and watch what it does with the freedom
The app ships with an AGENTS.md
covering worked REPL examples, bean names, credentials, and a quick smoke-test —
a solid starting point for an agentic session.
skills/livewire/SKILL.md is the most important file
in this repository if you're working with an AI agent.
It covers the full API across all eight namespaces, worked examples, known pitfalls, and escalation strategies for debugging without restarts. It's written for agents — but it's perfectly readable by humans too.
Without SKILL.md in the agent's context, cooperation will be poor.
The agent will hallucinate method signatures, call things that don't exist,
and make sloppy guesses about behaviour it could just... ask the live app about.
With it, the agent knows exactly what tools it has, how to use them, and what to watch out for.
The skill is a directory — skills/livewire/ — containing SKILL.md and the bin/
CLI wrapper scripts (lw-jpa-query, lw-call-endpoint, lw-sql, etc.). Copy the whole
thing, not just the markdown file.
First, clone this repository (you don't need to build anything — you just need the files):
git clone https://github.com/brdloush/livewireThen copy the skill directory into your Spring Boot project:
# Per-project — checked in alongside the app that uses Livewire (recommended)
cp -r livewire/skills/livewire /your/spring-app/.claude/skills/livewire
# Or symlink it so upgrades are instant (just git pull in the livewire clone)
ln -s /path/to/livewire/skills/livewire /your/spring-app/.claude/skills/livewireMake sure the bin/ scripts are executable:
chmod +x /your/spring-app/.claude/skills/livewire/bin/*Even if the file is in place, explicitly telling the agent to load the skill at the start of a session gets much better results than hoping it gets picked up passively:
"Load the Livewire skill."
That one sentence changes the entire session. The agent switches from guessing to probing. From static analysis to live questions. From "I think the query might be..." to "I just measured it — 481 queries. Here's why, and here's the fix."
Quick namespace cheatsheet:
| Namespace | Require as | What it does |
|---|---|---|
net.brdloush.livewire.core |
lw |
Beans, transactions, run-as, properties |
net.brdloush.livewire.query |
q |
Raw SQL, diff-entity |
net.brdloush.livewire.trace |
trace |
SQL tracing, N+1 detection |
net.brdloush.livewire.hot-queries |
hq |
Live @Query swap + restore |
net.brdloush.livewire.query-watcher |
qw |
Auto-apply @Query on recompile |
net.brdloush.livewire.introspect |
intro |
Endpoints, entities, schema |
net.brdloush.livewire.jpa-query |
jpa |
JPQL via live EntityManager, smart entity serialization |
net.brdloush.livewire.mvc |
mvc |
Response serialization via Spring MVC's Jackson ObjectMapper |
- 📖 Read the full SKILL.md — every function, pitfall, and worked example across all eight namespaces
- 🚀 Try the bloated-shelf demo app — a realistic N+1 scenario ready to investigate
- 🐛 Found a bug or have an idea? Open an issue
Java Troubleshooting on Steroids with Clojure REPL by Jakub Holý (2019) — the idea that you can wire a Clojure REPL into a running JVM and talk to it live was the seed this project grew from. Worth a read.
Don't touch live wires in production. But in dev? Grab on.
