<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://metaredux.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://metaredux.com/" rel="alternate" type="text/html" /><updated>2026-11-01T09:06:54+00:00</updated><id>https://metaredux.com/feed.xml</id><title type="html">Meta Redux</title><subtitle>Writings on programming and open-source by Bozhidar Batsov, creator of RuboCop, CIDER, and Prelude.</subtitle><author><name>Bozhidar Batsov</name></author><entry><title type="html">nREPL, Right in Your Browser</title><link href="https://metaredux.com/posts/2026/11/01/nrepl-right-in-your-browser.html" rel="alternate" type="text/html" title="nREPL, Right in Your Browser" /><published>2026-11-01T09:06:00+00:00</published><updated>2026-11-01T09:06:00+00:00</updated><id>https://metaredux.com/posts/2026/11/01/nrepl-right-in-your-browser</id><content type="html" xml:base="https://metaredux.com/posts/2026/11/01/nrepl-right-in-your-browser.html"><![CDATA[<p>Last week <a href="https://nrepl.org">nrepl.org</a> got a makeover. The docs moved to
<a href="https://docs.nrepl.org">docs.nrepl.org</a>, and nrepl.org itself became a
landing page that explains what nREPL is and how its protocol works, with
“wire windows” showing real exchanges between a client and a server, as EDN or
as the bencode that actually goes over the wire. Reading about a protocol only
gets you so far, though, so the page now also has a <a href="https://nrepl.org/#try">“Try it”
section</a> with a real nREPL server running right in
your browser. There’s one in this post as well! In the rest of it I’ll tell
you how it came about, why it was harder than it looks, and how you can use
the server behind it for your own projects.</p>

<!--more-->

<p>Here it is:</p>

<!-- A REPL talking to nrepl-sci (https://github.com/nrepl/nrepl-sci), an
     nREPL server that runs in Web Workers. The worker takes the bencode of
     each request and posts back the bencode of each reply, so everything
     below the markup is a small nREPL client. -->
<div class="nrepl-demo" data-worker="/assets/js/nrepl-sci-worker-0.1.0.js">
  <div class="nrepl-demo-log" aria-live="polite"></div>
  <form class="nrepl-demo-input">
    <label for="nrepl-demo-code">user=&gt;</label>
    <input id="nrepl-demo-code" type="text" placeholder="(+ 1 2)" spellcheck="false" autocomplete="off" autocapitalize="off" data-1p-ignore="" data-lpignore="true" data-bwignore="" />
  </form>
  <div class="nrepl-demo-bar">
    <button type="button" data-code="(+ 1 2)">(+ 1 2)</button>
    <button type="button" data-code="(println &quot;Hello from nREPL!&quot;)">(println "Hello from nREPL!")</button>
    <button type="button" data-code="(map inc (range 10))">(map inc (range 10))</button>
    <button type="button" data-code="(doc map)">(doc map)</button>
    <button type="button" data-code="(range)">(range)</button>
    <button type="button" class="nrepl-demo-interrupt" hidden="">interrupt</button>
    <label class="nrepl-demo-wire"><input type="checkbox" /> show the messages</label>
  </div>
</div>
<style>
  .nrepl-demo { margin: 0 0 15px; border: 1px solid #e8e8e8; border-radius: 3px; background: #eef; font: 14px/1.5 Menlo, Monaco, Consolas, monospace; }
  .nrepl-demo-log { max-height: 20em; overflow: auto; padding: 8px 12px 0; white-space: pre-wrap; overflow-wrap: anywhere; }
  .nrepl-demo-log:empty { display: none; }
  .nrepl-demo-log .input { color: #828282; }
  .nrepl-demo-log .value { color: #1a7f37; }
  .nrepl-demo-log .err { color: #c62828; }
  .nrepl-demo-log .wire { color: #6a737d; font-size: 12px; }
  .nrepl-demo-input { display: flex; gap: 8px; padding: 8px 12px; }
  .nrepl-demo-input input { flex: 1; min-width: 0; padding: 0; border: 0; outline: none; background: none; font: inherit; }
  .nrepl-demo-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 8px 12px; border-top: 1px solid #e8e8e8; }
  .nrepl-demo-bar button { padding: 1px 8px; border: 1px solid #ccc; border-radius: 3px; background: #fff; font: 12px Menlo, Monaco, Consolas, monospace; cursor: pointer; }
  .nrepl-demo-bar .nrepl-demo-interrupt { border-color: #c62828; color: #c62828; }
  .nrepl-demo-wire { margin-left: auto; font-size: 12px; color: #6a737d; }
</style>

<script>
(() => {
  const demo = document.querySelector(".nrepl-demo");
  const log = demo.querySelector(".nrepl-demo-log");
  const input = demo.querySelector("#nrepl-demo-code");
  const prompt = demo.querySelector("label[for]");
  const interruptButton = demo.querySelector(".nrepl-demo-interrupt");
  const showWire = demo.querySelector(".nrepl-demo-wire input");
  const encoder = new TextEncoder();
  const decoder = new TextDecoder();

  // nREPL's messages go over the wire as bencode: dictionaries of strings,
  // integers and lists.
  const encode = (x) =>
    typeof x === "number" ? `i${x}e`
    : typeof x === "string" ? `${encoder.encode(x).length}:${x}`
    : Array.isArray(x) ? `l${x.map(encode).join("")}e`
    : `d${Object.keys(x).sort().map((k) => encode(k) + encode(x[k])).join("")}e`;

  function decode(bytes) {
    let i = 0;
    const next = () => {
      const c = bytes[i];
      if (c === 0x69) { // i42e
        const end = bytes.indexOf(0x65, i);
        const n = Number(decoder.decode(bytes.subarray(i + 1, end)));
        i = end + 1;
        return n;
      }
      if (c === 0x6c || c === 0x64) { // l...e and d...e
        i++;
        const items = [];
        while (bytes[i] !== 0x65) items.push(next());
        i++;
        if (c === 0x6c) return items;
        const dict = {};
        for (let k = 0; k < items.length; k += 2) dict[items[k]] = items[k + 1];
        return dict;
      }
      const colon = bytes.indexOf(0x3a, i); // 5:hello
      const start = colon + 1;
      i = start + Number(decoder.decode(bytes.subarray(i, colon)));
      return decoder.decode(bytes.subarray(start, i));
    };
    return next();
  }

  // The messages, shown as EDN
  const edn = (x) =>
    Array.isArray(x) ? `[${x.map(edn).join(" ")}]`
    : typeof x === "object" ? `{${Object.entries(x).map(([k, v]) => `:${k} ${edn(v)}`).join(", ")}}`
    : typeof x === "string" ? JSON.stringify(x)
    : String(x);

  let scrolling = false;
  function print(kind, text) {
    const line = document.createElement("div");
    line.className = kind;
    line.textContent = text.replace(/\n$/, "");
    log.append(line);
    while (log.childElementCount > 500) log.firstElementChild.remove();
    if (!scrolling) {
      scrolling = true;
      requestAnimationFrame(() => { scrolling = false; log.scrollTop = log.scrollHeight; });
    }
  }

  let worker = null;
  let started = null;
  let session = null;
  let nextId = 0;
  let ns = "user";
  let running = null;
  const handlers = {};

  // Sends a request, calling `handle` with each of its replies.
  function send(msg, handle) {
    msg.id = String(++nextId);
    if (session) msg.session = session;
    if (handle) handlers[msg.id] = handle;
    if (showWire.checked) print("wire", `→ ${edn(msg)}`);
    worker.postMessage(encoder.encode(encode(msg)));
    return msg.id;
  }

  function receive(event) {
    const msg = decode(event.data);
    if (showWire.checked) print("wire", `← ${edn(msg)}`);
    const handle = handlers[msg.id];
    if (msg.status?.includes("done")) delete handlers[msg.id];
    if (handle) handle(msg);
  }

  // The server starts the first time it's needed, with a session of our own.
  function start() {
    started ??= new Promise((resolve) => {
      worker = new Worker(demo.dataset.worker);
      worker.onmessage = receive;
      worker.onerror = () => print("err", "The server didn't start.");
      send({ op: "clone" }, (msg) => resolve(session = msg["new-session"]));
    });
    return started;
  }

  async function evaluate(code) {
    if (running) return;
    running = true;
    print("input", `${ns}=> ${code}`);
    await start();
    running = send({ op: "eval", code }, (msg) => {
      if (msg.out) print("out", msg.out);
      if (msg.err) print("err", msg.err);
      if ("value" in msg) print("value", msg.value);
      if (msg.ns) prompt.textContent = `${ns = msg.ns}=>`;
      if (msg.status?.includes("done")) running = null;
      interruptButton.hidden = !running;
    });
    interruptButton.hidden = false;
  }

  demo.querySelector("form").addEventListener("submit", (event) => {
    event.preventDefault();
    if (running || !input.value.trim()) return;
    evaluate(input.value.trim());
    input.value = "";
  });
  demo.querySelectorAll("[data-code]").forEach((button) =>
    button.addEventListener("click", () => evaluate(button.dataset.code)));
  interruptButton.addEventListener("click", () =>
    send({ op: "interrupt", "interrupt-id": running }));
  input.addEventListener("focus", start);
})();
</script>

<p>Go ahead, evaluate something! Tick “show the messages” to see the requests
and the responses going back and forth, and try <code class="language-plaintext highlighter-rouge">(range)</code> if you’re curious
what happens to code that never finishes.</p>

<h2 id="picking-an-approach">Picking an approach</h2>

<p>An interactive nREPL playground always runs into the same problem: you need
an nREPL server somewhere, and a server that evaluates code from random people
on the internet is just about the worst thing you can host. (It would also
cost money that I’d rather spend on something else.) So the server had to run
in the browser, and I looked at a few ways to get there.</p>

<p>The first one was the real thing. <a href="https://cheerpj.com">CheerpJ</a> is a JVM
that runs in the browser, and nREPL 1.8 and Clojure 1.12 worked on it
remarkably well. Every op I tried worked, <code class="language-plaintext highlighter-rouge">stdin</code> included! The
problem was everything around that - about 8 seconds to a ready server on a
fast machine, about 4.5MB to download, a busy loop that freezes the whole page
unless you put the JVM in a Web Worker, and an <code class="language-plaintext highlighter-rouge">interrupt</code> that works only on
code that’s sleeping.<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> That’s a lot to ask of someone who just opened a
landing page.</p>

<p>The second one was <a href="https://github.com/babashka/scittle">scittle</a>, which runs
<a href="https://github.com/babashka/sci">SCI</a> (Michiel Borkent’s Clojure interpreter)
in the browser and comes with an nREPL server,
<a href="https://github.com/babashka/sci.nrepl">sci.nrepl</a>. It solves a different
problem, though - it lets your editor evaluate code in a page, through a relay
that runs in babashka or on the JVM, and its replies differ from nREPL’s in
quite a few places. (I’ve filed <a href="https://github.com/babashka/sci.nrepl/issues">some
issues</a> about those.)
<a href="https://github.com/babashka/nbb">nbb</a> has a pretty complete nREPL server of
its own, but it runs only on Node.</p>

<p>None of those fit, so I ended up writing a small nREPL server for SCI that
runs entirely in a web page, with the page as its client -
<a href="https://github.com/nrepl/nrepl-sci">nrepl-sci</a>. SCI does all the heavy
lifting, and evaluation, completion and docs lookup took just a few hundred
lines of ClojureScript. The whole thing is about 230KB gzipped and it’s ready
in a fraction of a second. Not bad, right?</p>

<h2 id="the-hard-part">The hard part</h2>

<p>A basic nREPL server is easy enough. The trouble starts with two ops most
people never think about - <code class="language-plaintext highlighter-rouge">interrupt</code> and <code class="language-plaintext highlighter-rouge">stdin</code>.</p>

<p>JavaScript gives you one thread, and SCI evaluates code in one go. While
<code class="language-plaintext highlighter-rouge">(loop [] (recur))</code> runs nothing else does, so the server can’t even read the
<code class="language-plaintext highlighter-rouge">interrupt</code> request that would stop it, and code calling <code class="language-plaintext highlighter-rouge">read-line</code> can’t
wait for input without blocking the very thread that would receive it. There’s
a third problem hiding in there as well. nREPL sessions are supposed to be
independent, so an eval in one session (say, your editor’s) shouldn’t hold up
another one (say, a test runner connected to the same server).</p>

<p>Here’s how nrepl-sci deals with all of that:</p>

<ul>
  <li>The server runs in two Web Workers. A supervisor takes the requests, queues
them, and answers <code class="language-plaintext highlighter-rouge">interrupt</code> and <code class="language-plaintext highlighter-rouge">stdin</code> itself, while an evaluator runs
SCI.</li>
  <li>The two share some memory (a <code class="language-plaintext highlighter-rouge">SharedArrayBuffer</code>) that serves as a
mailbox. The evaluator waits for mail with <code class="language-plaintext highlighter-rouge">Atomics.wait</code>, which blocks a
worker without spinning, so <code class="language-plaintext highlighter-rouge">read-line</code> waits for its input there, much like
it would on the JVM.</li>
  <li>SCI calls a hook of your choice on every function call and every <code class="language-plaintext highlighter-rouge">recur</code>.
nrepl-sci uses it to check its mailbox, to see if it’s been interrupted, or
if there’s a request from another session it could take in between. Checking
on every call made code noticeably slower, so it checks on every 256th call,
which still means every few microseconds. Benchmarks run about as fast as
without any of this.</li>
  <li>Endless seqs like <code class="language-plaintext highlighter-rouge">(range)</code> and <code class="language-plaintext highlighter-rouge">(iterate inc 0)</code> check as they go, so
<code class="language-plaintext highlighter-rouge">(count (range))</code> stops when you interrupt it.</li>
  <li>Some code never calls back into SCI - a regex that backtracks forever, for
instance. The supervisor gives it a second to stop, then kills the evaluator
and starts a new one. Your sessions survive that, but everything you
evaluated in them is gone.</li>
</ul>

<p>In the end the supervisor, the evaluator and the mailbox took more code than
the nREPL server itself, which tells you something about where the
complexity is.</p>

<p><strong>Note:</strong> All of this works only when the page is <a href="https://web.dev/articles/cross-origin-isolation-guide">cross-origin
isolated</a>, as browsers
hand out <code class="language-plaintext highlighter-rouge">SharedArrayBuffer</code> only to pages served with the
<code class="language-plaintext highlighter-rouge">Cross-Origin-Opener-Policy</code> and <code class="language-plaintext highlighter-rouge">Cross-Origin-Embedder-Policy</code> headers.</p>

<p>Sadly, GitHub Pages, which hosts both nrepl.org and this blog, can’t send
those. Without them nrepl-sci falls back to doing one thing at a time:
<code class="language-plaintext highlighter-rouge">read-line</code> throws, and interrupting code means starting over, which is exactly
what you saw if you tried <code class="language-plaintext highlighter-rouge">(range)</code> above. Evaluation, output, errors,
completion and <code class="language-plaintext highlighter-rouge">doc</code> work the same either way, though.</p>

<h2 id="testing-with-proof">Testing with proof</h2>

<p>Back in 2023 I listed the lack of a protocol compatibility test suite among
the <a href="/posts/2023/03/26/bad-nrepl.html">things people hate about nREPL</a>,
and asked if one of you was looking for a fun small OSS project. Eventually I
took my own advice and started <a href="https://github.com/nrepl/proof">proof</a>, and
nrepl-sci is the first server I’ve written with proof around from day one. It
made a huge difference! Building an nREPL server used to involve a lot of
guessing, as the protocol is small, but what clients actually expect from a
server is spread across their source code.</p>

<p>A proof profile is a short TOML file that tells proof how to start your server
and how to say a few things in its language: a value, some output, an
exception, reading a line, code that runs until it’s stopped. <code class="language-plaintext highlighter-rouge">proof run</code>
takes it from there, with checks covering the protocol and the exact requests
CIDER, Calva, Conjure, vim-fireplace, REPLy and neorepl send. When a check
fails, proof also tells you why it matters:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FAIL   robust.sessions-at-once      An eval in one session doesn't hold up the others
       clone request got no reply with "done" within 10s
       why: Each client has sessions of its own (e.g. an editor and neorepl,
       connected to the same server), so a server that runs one eval at a time
       leaves every other client waiting for whatever the code in one of them
       does.
</code></pre></div></div>

<p>That’s a check you can easily shrug off when you’re writing a server for a
playground, and it’s the reason nrepl-sci takes requests in between at all.
Working on the server mostly came down to running <code class="language-plaintext highlighter-rouge">make proof</code> after every
change. Today nrepl-sci passes all 73 checks that apply to it (the other 5 are
about things like vim-fireplace, which needs Java). Without shared memory it
fails exactly two: <code class="language-plaintext highlighter-rouge">stdin.need-input</code> and <code class="language-plaintext highlighter-rouge">robust.sessions-at-once</code>. I guess
that’s a pretty good summary of what you give up without cross-origin
isolation.</p>

<p>If you’re working on an nREPL server of your own, I can’t recommend proof
enough. It also comes with a GitHub Action, so your CI can run it as well.</p>

<h2 id="the-tradeoffs">The tradeoffs</h2>

<p>Nothing comes for free, and nrepl-sci is no exception:</p>

<ul>
  <li>It’s SCI, not Clojure. You’re getting ClojureScript as interpreted by SCI,
so there are no Java classes, no JVM libraries, and some corners of the
language behave differently. That’s fine for a playground, but not for
anything that needs your real project.</li>
  <li>There’s no JavaScript interop. SCI blocks it unless you allow it, and
nrepl-sci doesn’t, so <code class="language-plaintext highlighter-rouge">(js/alert "hi")</code> and even <code class="language-plaintext highlighter-rouge">(.toUpperCase "hi")</code> fail.
Admittedly, that’s a bit annoying, but it’s also what makes nrepl-sci safe
to embed anywhere, as the code people type can’t touch the page, or even the
worker it runs in. (<code class="language-plaintext highlighter-rouge">clojure.string/upper-case</code> works just fine.)</li>
  <li>A request from another session runs on top of the code it interrupted, so
that code can’t stop until the request is done, and a <code class="language-plaintext highlighter-rouge">read-line</code> taken in
between holds it until it gets its input. You’d need two clients fighting
over the same server to notice.</li>
  <li>Starting over loses your definitions, which matters most without
cross-origin isolation, where every interrupt starts over.</li>
  <li>It’s about 1MB of JavaScript (230KB gzipped), loaded the first time you use
the REPL. That’s nothing compared to CheerpJ, but it’s not free either.</li>
</ul>

<h2 id="using-nrepl-sci-yourself">Using nrepl-sci yourself</h2>

<p>nrepl-sci was built for nrepl.org, but nothing in it is specific to the site.</p>

<p>You can embed a REPL in a page - documentation, a tutorial, a blog post. The
worker takes the bencode of each request as a <code class="language-plaintext highlighter-rouge">Uint8Array</code> and posts back the
bencode of each reply, so the whole integration boils down to something like
this:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">worker</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Worker</span><span class="p">(</span><span class="dl">"</span><span class="s2">/assets/js/nrepl-sci-worker-0.1.0.js</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">worker</span><span class="p">.</span><span class="nx">onmessage</span> <span class="o">=</span> <span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">decode</span><span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">data</span><span class="p">));</span>
<span class="nx">worker</span><span class="p">.</span><span class="nx">postMessage</span><span class="p">(</span><span class="k">new</span> <span class="nx">TextEncoder</span><span class="p">().</span><span class="nx">encode</span><span class="p">(</span><span class="nx">encode</span><span class="p">({</span> <span class="na">op</span><span class="p">:</span> <span class="dl">"</span><span class="s2">eval</span><span class="dl">"</span><span class="p">,</span> <span class="na">code</span><span class="p">:</span> <span class="dl">"</span><span class="s2">(+ 1 2)</span><span class="dl">"</span><span class="p">,</span> <span class="na">id</span><span class="p">:</span> <span class="dl">"</span><span class="s2">1</span><span class="dl">"</span> <span class="p">})));</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">encode</code> and <code class="language-plaintext highlighter-rouge">decode</code> are a few dozen lines of bencode, and the REPL in this
post is about 170 lines in total, styles included - <a href="https://github.com/bbatsov/metaredux/blob/master/_includes/nrepl-sci-demo.html">have a
look</a>.
Grab <code class="language-plaintext highlighter-rouge">nrepl-sci-worker.js</code> from the <a href="https://github.com/nrepl/nrepl-sci/releases/latest">latest
release</a> and serve it from
your own site, as browsers start workers only from the page’s own origin. If
you can send the cross-origin isolation headers, you get <code class="language-plaintext highlighter-rouge">stdin</code> and proper
interrupts for free. nrepl.org’s
<a href="https://github.com/nrepl/nrepl.org/blob/master/assets/js/playground.js">playground</a>
is a bigger example, with completion, history and the wire windows.</p>

<p>You can also test nREPL clients without a JVM. The Node build is a regular
nREPL server on a socket (<code class="language-plaintext highlighter-rouge">node nrepl-sci.js --port 7888</code>) that’s up in about
a tenth of a second, which makes it handy for the test suites of clients and
editor plugins.</p>

<p>Finally, if you’re after an nREPL server for some other language that runs on
a single thread, the supervisor and the mailbox might give you a few ideas.
The server itself knows nothing about threads or sockets, and the trick with
the mailbox works for any interpreter that can call back now and then while
it runs.</p>

<h2 id="whats-next">What’s next</h2>

<p>The obvious next step is getting cross-origin isolation on nrepl.org. A
service worker can add the headers GitHub Pages won’t send, at the cost of a
reload when it’s first installed, so I might try that on a page of its own.
I’d also like to allow some harmless interop, so that <code class="language-plaintext highlighter-rouge">(.toUpperCase "hi")</code>
works as people expect, and to publish nrepl-sci on npm.</p>

<h2 id="epilogue">Epilogue</h2>

<p>Back in 2018 I wrote that there was <a href="/posts/2018/10/29/nrepl-redux.html">“the big dream to port nREPL to
ClojureScript”</a>, so it could be used in
self-hosted environments, and in 2023 I was still hoping we’d see more native
ClojureScript nREPL servers. nrepl-sci isn’t quite that dream - it’s a much
smaller nREPL server for SCI - but it’s a step in that direction.</p>

<p>Big thanks to Michiel Borkent for SCI, which did most of the hard work here.
None of this would exist without it! And, as usual, special thanks to
<a href="https://www.clojuriststogether.org/">Clojurists Together</a> for supporting my
work on nREPL!</p>

<p>That’s all I have for you today. Play with the REPL, try to break it, and let
me know how it went! Keep hacking!</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>CheerpJ’s free tier also covers only non-commercial open-source projects that load it from Leaning Technologies’ CDN. That would have been fine for nrepl.org, but it’s one more string attached. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Bozhidar Batsov</name></author><category term="posts" /><category term="Clojure" /><category term="nREPL" /><category term="nrepl-sci" /><summary type="html"><![CDATA[Last week nrepl.org got a makeover. The docs moved to docs.nrepl.org, and nrepl.org itself became a landing page that explains what nREPL is and how its protocol works, with “wire windows” showing real exchanges between a client and a server, as EDN or as the bencode that actually goes over the wire. Reading about a protocol only gets you so far, though, so the page now also has a “Try it” section with a real nREPL server running right in your browser. There’s one in this post as well! In the rest of it I’ll tell you how it came about, why it was harder than it looks, and how you can use the server behind it for your own projects.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://metaredux.com/assets/og-image.png" /><media:content medium="image" url="https://metaredux.com/assets/og-image.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">neorepl 0.4: Tests, Inspector, Tracing and Agents</title><link href="https://metaredux.com/posts/2026/10/30/neorepl-0-4.html" rel="alternate" type="text/html" title="neorepl 0.4: Tests, Inspector, Tracing and Agents" /><published>2026-10-30T12:30:00+00:00</published><updated>2026-10-30T12:30:00+00:00</updated><id>https://metaredux.com/posts/2026/10/30/neorepl-0-4</id><content type="html" xml:base="https://metaredux.com/posts/2026/10/30/neorepl-0-4.html"><![CDATA[<p><a href="https://github.com/nrepl/neorepl/releases/tag/v0.4.0">neorepl 0.4</a> came out
last week, and since I never got around to writing about 0.2 and 0.3, it
feels like a good moment to look back at what happened since the <a href="/posts/2026/10/10/neorepl-a-terminal-repl-for-clojure-and-friends.html">original
announcement</a>.
Three releases in two weeks, and neorepl now has most of what I listed under
“What’s next” back then - a test runner, a full-screen inspector and
stacktrace browser, an MCP server and a skill for coding agents - plus a few
things that weren’t on the list at all, like tracing and peeking into your
coding agent’s REPL session.</p>

<!--more-->

<h2 id="running-tests">Running tests</h2>

<p><img src="/assets/images/neorepl-tests.gif" alt="Running tests in neorepl" /></p>

<p><code class="language-plaintext highlighter-rouge">,test</code> in the REPL, or <code class="language-plaintext highlighter-rouge">neorepl test</code> in your shell, runs the tests in the
namespaces you name, the tests you name or, without either, the whole
project’s. Failures come with their file and line, and errors with a stack
trace that stops at the test that hit them, instead of wandering off into
<code class="language-plaintext highlighter-rouge">clojure.test</code>’s guts. <code class="language-plaintext highlighter-rouge">,retest</code> (<code class="language-plaintext highlighter-rouge">neorepl retest</code>) reruns only what didn’t
pass, which is exactly what you want while you’re fixing things.</p>

<p>As usual, the server does the heavy lifting. cider-nrepl runs the tests when
it’s around, and so does babashka’s nREPL server, which bundles CIDER’s test
ops these days. Without either, neorepl falls back to plain <code class="language-plaintext highlighter-rouge">clojure.test</code>,
as long as you tell it which namespaces to test. And in a ClojureScript
session the same commands run your ClojureScript tests.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>neorepl <span class="nb">test</span>
<span class="nv">$ </span>neorepl <span class="nb">test </span>my.app-test my.app.db-test
<span class="nv">$ </span>neorepl <span class="nb">test</span> <span class="nt">--format</span> json my.app-test/start-test
</code></pre></div></div>

<h2 id="stack-traces-and-the-inspector">Stack traces and the inspector</h2>

<p><code class="language-plaintext highlighter-rouge">,stacktrace</code> shows the last error full-screen. Pick a frame, press Enter,
and its source opens in your <code class="language-plaintext highlighter-rouge">$EDITOR</code> at that line (as a copy, for code in a
jar), and when you quit the editor you’re back where you were. By default you
see only the frames that matter, and <code class="language-plaintext highlighter-rouge">a</code> brings back the rest - the REPL’s own
machinery and the repeated frames of the same function. With cider-nrepl, <code class="language-plaintext highlighter-rouge">i</code>
inspects the cause you’re looking at and <code class="language-plaintext highlighter-rouge">d</code> its ex-data. After a test run
with errors, <code class="language-plaintext highlighter-rouge">,stacktrace</code> shows those, each under the test it came from.
(You can see that at the end of the GIF above.)</p>

<p><code class="language-plaintext highlighter-rouge">,inspect</code> puts cider-nrepl’s inspector (babashka has one too) in full-screen:</p>

<p><img src="/assets/images/neorepl-inspect.gif" alt="The neorepl inspector" /></p>

<p>The arrow keys (or <code class="language-plaintext highlighter-rouge">j</code> and <code class="language-plaintext highlighter-rouge">k</code>) move around, Enter digs into a value and
Backspace goes back up, <code class="language-plaintext highlighter-rouge">[</code> and <code class="language-plaintext highlighter-rouge">]</code> page through big collections, <code class="language-plaintext highlighter-rouge">v</code> shows
an object’s fields, <code class="language-plaintext highlighter-rouge">P</code> toggles pretty printing and <code class="language-plaintext highlighter-rouge">g</code> refreshes values that
change, like an atom’s. The handiest one is <code class="language-plaintext highlighter-rouge">d</code>, which defines whatever
you’re looking at as a var. Dig five levels deep into some big API response, press
<code class="language-plaintext highlighter-rouge">d</code>, give it a name, and you can play with that piece in the REPL right away.</p>

<p><code class="language-plaintext highlighter-rouge">neorepl inspect</code> does the same one step at a time, for scripts and agents,
since the inspector keeps its state in the session:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>neorepl inspect <span class="s1">'(System/getProperties)'</span>
<span class="nv">$ </span>neorepl inspect <span class="nt">--push</span> 3
<span class="nv">$ </span>neorepl inspect <span class="nt">--def</span> props
</code></pre></div></div>

<h2 id="tracing">Tracing</h2>

<p><img src="/assets/images/neorepl-trace.gif" alt="Tracing in neorepl" /></p>

<p><code class="language-plaintext highlighter-rouge">,trace</code> traces a function, or every function in a namespace, and from then
on each call shows its arguments and what it returned, nested in the calls
it’s part of. It’s often the quickest way to see what some code is actually
doing, and there are no <code class="language-plaintext highlighter-rouge">println</code>s to forget to remove afterwards. Calls on
other threads, like an HTTP handler’s or a future’s, show up above the prompt
too. <code class="language-plaintext highlighter-rouge">,trace</code> without arguments lists what’s traced and <code class="language-plaintext highlighter-rouge">,untrace</code> stops
tracing. Scripts and agents get <code class="language-plaintext highlighter-rouge">neorepl trace</code>, with the calls showing up in
the output of the <code class="language-plaintext highlighter-rouge">neorepl eval</code> that made them. Tracing needs cider-nrepl
on the server.</p>

<p>(Fun fact: tracing a function that the tracing itself calls, like
<code class="language-plaintext highlighter-rouge">clojure.core/map</code>, used to take the whole server down. neorepl refuses to
trace those, and as of orchard 0.48 the server no longer falls over on them
anyway.)</p>

<h2 id="coding-agents">Coding agents</h2>

<p>0.3 was all about coding agents. The CLI was usable by agents from day one,
but now they get a lot more help:</p>

<ul>
  <li>
    <p><code class="language-plaintext highlighter-rouge">neorepl mcp</code> serves neorepl’s features as MCP tools over stdio, for agents
that would rather call tools than run commands. With Claude Code it’s a
one-liner:</p>

    <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>claude mcp add neorepl <span class="nt">--</span> neorepl mcp
</code></pre></div>    </div>

    <p>The tools are <code class="language-plaintext highlighter-rouge">eval</code>, <code class="language-plaintext highlighter-rouge">load</code>, <code class="language-plaintext highlighter-rouge">doc</code>, <code class="language-plaintext highlighter-rouge">source</code>, <code class="language-plaintext highlighter-rouge">where</code>, <code class="language-plaintext highlighter-rouge">apropos</code>,
<code class="language-plaintext highlighter-rouge">macroexpand</code>, <code class="language-plaintext highlighter-rouge">reload</code>, <code class="language-plaintext highlighter-rouge">test</code>, <code class="language-plaintext highlighter-rouge">retest</code>, <code class="language-plaintext highlighter-rouge">pst</code>, <code class="language-plaintext highlighter-rouge">interrupt</code>, <code class="language-plaintext highlighter-rouge">check</code> and
<code class="language-plaintext highlighter-rouge">ports</code>. That’s fewer than the CLI’s commands on purpose: an agent pays for
every tool’s description in its context on every turn, whether it uses the
tool or not. Evaluations get cut at 10,000 characters and interrupted after
60 seconds by default, so a stray <code class="language-plaintext highlighter-rouge">(range)</code> or an infinite loop can’t eat
the agent’s context or its session.</p>
  </li>
  <li>A <a href="https://github.com/nrepl/neorepl/blob/main/skills/neorepl/SKILL.md">skill</a>
teaches agents the workflow I’d like them to follow: after editing a file,
check its parens, load it, run its tests, and read the stack trace when
something fails. It costs nothing until the agent actually works on Clojure
code. <a href="https://github.com/nrepl/neorepl/blob/main/doc/agents.md">doc/agents.md</a>
explains how to set it up with Claude Code, Codex, Gemini CLI, Cursor and
VS Code.</li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">neorepl check --fix</code> repairs unbalanced delimiters the way parinfer’s
indent mode would, trusting the indentation over the parens. Mismatched
parens are still the most common way for an agent to break a Clojure file,
and now fixing them doesn’t take another trip through the model:</p>

    <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>neorepl check src/demo/orders.clj
src/demo/orders.clj:3:1: Unclosed <span class="o">(</span>
<span class="nv">$ </span>neorepl check <span class="nt">--fix</span> src/demo/orders.clj
src/demo/orders.clj:3:1: Unclosed <span class="o">(</span>, repaired from the indentation
</code></pre></div>    </div>
  </li>
</ul>

<p>The CLI and the MCP tools share named sessions, the one called <code class="language-plaintext highlighter-rouge">agent</code> unless
told otherwise, which brings me to my favourite addition in 0.4.</p>

<h2 id="looking-over-your-agents-shoulder">Looking over your agent’s shoulder</h2>

<p><img src="/assets/images/neorepl-sessions.gif" alt="Stepping into an agent's session" /></p>

<p><code class="language-plaintext highlighter-rouge">,sessions</code> lists the server’s named sessions, and <code class="language-plaintext highlighter-rouge">,sessions agent</code> steps
into the agent’s, with its name in the prompt. Everything the agent left
behind is right there: its namespace, its <code class="language-plaintext highlighter-rouge">*1</code>, its <code class="language-plaintext highlighter-rouge">*e</code>. So when an agent
tells you that a test fails with a <code class="language-plaintext highlighter-rouge">NullPointerException</code>, you can look at
the exception yourself instead of asking it to paste the stack trace.
<code class="language-plaintext highlighter-rouge">,sessions -</code> takes you back to your own session.</p>

<p>The lookups take <code class="language-plaintext highlighter-rouge">--session</code> too, so <code class="language-plaintext highlighter-rouge">neorepl doc --session agent foo</code> looks
up <code class="language-plaintext highlighter-rouge">foo</code> the way that session sees it, in its namespace and its language,
ClojureScript included.</p>

<h2 id="the-repl-round-two">The REPL, round two</h2>

<p>0.4 also spent a lot of time on the REPL itself:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">,ns</code> switches namespaces, loading the one you name if it isn’t loaded
yet, and lists the project’s namespaces when you don’t name one.</li>
  <li>C-x C-s shows the source of the symbol at the cursor and C-x C-a the vars
whose names contain it, next to the C-x C-d for docs that was there from
the start. All three come from rebel-readline.</li>
  <li><code class="language-plaintext highlighter-rouge">,doc</code> and the arglists that show up as you type cover Java classes,
methods and fields too, with their Javadoc as text (that one needs
cider-nrepl).</li>
  <li><code class="language-plaintext highlighter-rouge">,edit</code> and the stacktrace browser open definitions that live in jars,
instead of telling you they can’t.</li>
  <li>Ctrl-C got a lot more reliable. An interrupt sent right after an
evaluation could get lost when the server hadn’t started running it yet,
the forms after an interrupted one ran anyway, and whatever an interrupted
evaluation printed later could show up in the next one’s output. All fixed.</li>
  <li>neorepl sends the forms you enter to the server one at a time now, which
is a big part of the interrupt fixes, and as a bonus piggieback evaluates
all of them, not only the first.</li>
  <li>On Linux, every evaluation used to wait 40ms on the server’s socket,
courtesy of TCP’s delayed ACKs. neorepl turns those off now, on the JVM
and on babashka 1.13.227 or newer, and evaluating a file with 100 forms
went from 4.5 seconds to about 150ms.</li>
  <li>Lots of error messages got friendlier: a build tool that isn’t installed,
a server that dies while starting, loading a file with unbalanced parens, a
named session the server lost when it restarted, and so on.</li>
</ul>

<p>So much for keeping neorepl small, by the way - the code has about doubled
since 0.1. In my defense, it does a lot more than twice as much.</p>

<h2 id="making-the-neighbours-better-round-two">Making the neighbours better, round two</h2>

<p>Just like last time, building neorepl turned up problems elsewhere, and a
good part of these two weeks went into fixing them at the source:</p>

<ul>
  <li>I filed a batch of issues for babashka’s nREPL server: test runs that hung
or killed the session, <code class="language-plaintext highlighter-rouge">:project?</code> being ignored, a session that never
answered again after its client disconnected, a couple of JLine classes
neorepl’s line editor needed. Michiel fixed every single one of them within
days, and they’re all in babashka 1.13.227. Thanks, Michiel!</li>
  <li>cider-nrepl 0.64 and orchard 0.48 came out this week with fixes for the
problems I ran into: what tests print now comes back with the test results
(it used to go to the connection of the session’s previous message, which
for a CLI is long gone), the test op no longer goes silent for a namespace
that isn’t loaded or runs every test when the one you asked for doesn’t
exist, ClojureScript tests run on shadow-cljs, a traced call that throws
shows up as such, and Javadoc no longer comes with stray spaces in it.
CIDER gets all of that too, which is my favourite kind of side effect.
<a href="https://github.com/nrepl/neorepl/releases/tag/v0.4.1">neorepl 0.4.1</a>,
out the day after this post, uses cider-nrepl 0.64 when it starts a server.</li>
  <li>In nREPL, the fix for an interrupted evaluation’s last response going to
the wrong connection is merged, and so is <code class="language-plaintext highlighter-rouge">TCP_NODELAY</code> on the server’s
sockets, which takes 100 evaluations with nREPL’s own client from 4.4
seconds to 90ms on Linux. Both will be in the next nREPL release.</li>
  <li>The piggieback improvements I mentioned last time are a bit behind
schedule. A lot of ClojureScript REPLs run on top of piggieback, so a
mistake there has a pretty big blast radius, and evaluating every form of a
message is something we never found a good way to do in the past. I’d
rather take my time with those than rush them out. neorepl isn’t waiting on
them, at least, since it sends the forms one at a time now.</li>
</ul>

<h2 id="whats-next">What’s next</h2>

<p>0.5 is about more runtimes and better navigation:</p>

<ul>
  <li>integration tests against real servers in CI: cider-nrepl and babashka
first, then shadow-cljs and piggieback with Node</li>
  <li>nbb and Basilisp in CI, with their gaps documented</li>
  <li>cross-references (<code class="language-plaintext highlighter-rouge">,refs</code>), on top of cider-nrepl’s <code class="language-plaintext highlighter-rouge">fn-refs</code> and
<code class="language-plaintext highlighter-rouge">fn-deps</code></li>
  <li>a full-screen namespace browser</li>
  <li>a couple of ideas borrowed from clojure-cli.repl: evaluating the form at the
cursor, and the last evaluation’s outcome and time in the prompt</li>
  <li>indenting continuation lines, and a way to pick the vi keymap</li>
</ul>

<p>Better Windows support, the one item from the 0.1 list that’s still open, is
planned for 0.6, together with easier ways to install neorepl.</p>

<h2 id="try-it">Try it</h2>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>bbin <span class="nb">install </span>io.github.nrepl/neorepl
<span class="nv">$ </span><span class="nb">cd </span>my-project
<span class="nv">$ </span>neorepl
</code></pre></div></div>

<p>The <a href="https://github.com/nrepl/neorepl/blob/main/CHANGELOG.md">CHANGELOG</a> has
everything I skipped here, and the <a href="https://github.com/nrepl/neorepl">README</a>
has the details. Please keep the issues and ideas coming!</p>

<p>Keep hacking!</p>]]></content><author><name>Bozhidar Batsov</name></author><category term="posts" /><category term="Clojure" /><category term="nREPL" /><category term="neorepl" /><summary type="html"><![CDATA[neorepl 0.4 came out last week, and since I never got around to writing about 0.2 and 0.3, it feels like a good moment to look back at what happened since the original announcement. Three releases in two weeks, and neorepl now has most of what I listed under “What’s next” back then - a test runner, a full-screen inspector and stacktrace browser, an MCP server and a skill for coding agents - plus a few things that weren’t on the list at all, like tracing and peeking into your coding agent’s REPL session.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://metaredux.com/assets/og-image.png" /><media:content medium="image" url="https://metaredux.com/assets/og-image.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">neorepl: A Terminal REPL for Clojure (and Friends)</title><link href="https://metaredux.com/posts/2026/10/10/neorepl-a-terminal-repl-for-clojure-and-friends.html" rel="alternate" type="text/html" title="neorepl: A Terminal REPL for Clojure (and Friends)" /><published>2026-10-10T09:38:00+00:00</published><updated>2026-10-10T09:38:00+00:00</updated><id>https://metaredux.com/posts/2026/10/10/neorepl-a-terminal-repl-for-clojure-and-friends</id><content type="html" xml:base="https://metaredux.com/posts/2026/10/10/neorepl-a-terminal-repl-for-clojure-and-friends.html"><![CDATA[<p>For more than a decade the Clojure REPL most people saw in a terminal was
<a href="https://github.com/trptcolin/reply">REPLy</a>, the one behind <code class="language-plaintext highlighter-rouge">lein repl</code>. It
served us well, but it’s showing its age, and I kept wishing for a terminal
REPL that felt like CIDER, without the Emacs part. So I finally built one.
<a href="https://github.com/nrepl/neorepl">neorepl</a> 0.1 is out today!</p>

<!--more-->

<h2 id="why-another-repl">Why another REPL?</h2>

<p>There’s no shortage of Clojure REPLs, so it’s fair to ask. Here’s what I was
after:</p>

<ul>
  <li>nREPL all the way down. neorepl only speaks nREPL, so it works with anything
that runs an nREPL server: Clojure, babashka, shadow-cljs, Basilisp,
ClojureCLR and so on. The smarts live on the server. When cider-nrepl is
there neorepl uses it, the same way CIDER does, and when it isn’t it falls
back to nREPL’s built-in ops, and then to evaluating a bit of code.</li>
  <li>A REPL that’s pleasant to use: a proper line editor, highlighting,
completion, arglists as you type, pretty printing, history per project and
a Ctrl-C that actually interrupts the evaluation.</li>
  <li>A CLI that’s just as good for scripts and coding agents. Every lookup the
REPL can do is also a command, with structured output and exit codes that
mean something.</li>
  <li>Something small and modern. Clojure 1.10+, nREPL and JLine 4 are the only
dependencies, and the same code runs on the JVM and on babashka. The whole
thing is about 3,300 lines right now, and I’d like to keep it that way.</li>
</ul>

<p><code class="language-plaintext highlighter-rouge">rebel-readline</code> and <code class="language-plaintext highlighter-rouge">bb repl --connect</code> are both great, and I borrowed from
them freely, but neither tries to be an nREPL-first tool with CIDER’s tooling,
jack-in and a CLI for agents. That combination is the whole point of neorepl.</p>

<p><strong>Update (2026-10-11):</strong> A few people asked how neorepl relates to the
<a href="https://github.com/clojure/clojure-cli.repl">Clojure CLI REPL</a>, which the
Clojure team released a couple of weeks ago, and to <code class="language-plaintext highlighter-rouge">bb repl --connect</code>. (I’m a
bit embarrassed to admit I hadn’t heard of the former until after I announced
neorepl.) Funny enough, all three are nREPL clients under the hood, but they’re
after pretty different things. In practical terms:</p>

<ul>
  <li>The Clojure CLI REPL runs only on the JVM and needs Clojure 1.12+. It ships
its own nREPL server, and its docs and data inspector are ops of that server,
so against any other server you can only evaluate code. It’s deps.edn only.
Its editing is really nice, though - structural editing, evaluating the form
at the cursor and a <code class="language-plaintext highlighter-rouge">datafy</code>-based inspector - and I’ll be borrowing some
ideas from it. It doesn’t do completion yet.</li>
  <li><code class="language-plaintext highlighter-rouge">bb repl --connect</code> puts babashka’s console REPL in front of any nREPL server,
with completion, arglists and docs, in very little code. It’s a great way to
poke at a running server, but it won’t start one for you.</li>
  <li>neorepl works with any nREPL server and gets the most out of the ones with
cider-nrepl (or babashka’s take on it), without any middleware of its own.
It starts a server for Leiningen, deps.edn, babashka and shadow-cljs projects,
follows you into ClojureScript, and everything the REPL does is also a
command with JSON output for scripts and agents. No structural editing yet,
however.</li>
</ul>

<p>So, if you want the fanciest line editing and you live in deps.edn land, give
the Clojure CLI REPL a try. If you want CIDER in your terminal, for you and
for your agents, that’s what neorepl is for. The neat part is that babashka’s
server bundles CIDER’s inspector and test runner ops these days, so neorepl’s
test runner (already on <code class="language-plaintext highlighter-rouge">main</code>) and the inspector that’s coming next work with
babashka out of the box.</p>

<h2 id="for-humans">For humans</h2>

<p>Here’s the REPL in action:</p>

<p><img src="/assets/images/neorepl-repl.gif" alt="The neorepl REPL" /></p>

<p>A few things worth pointing out:</p>

<ul>
  <li>Enter only submits the input once its forms are complete, so pasting a whole
function just works.</li>
  <li>TAB completes, with the candidates’ types next to them, and the arglist of
the function you’re calling shows up after the cursor.</li>
  <li>Values get pretty printed on the server and highlighted on the client.</li>
  <li>Commands start with a comma, the CIDER way: <code class="language-plaintext highlighter-rouge">,doc</code>, <code class="language-plaintext highlighter-rouge">,source</code>, <code class="language-plaintext highlighter-rouge">,apropos</code>,
<code class="language-plaintext highlighter-rouge">,macroexpand</code>, <code class="language-plaintext highlighter-rouge">,pst</code>, <code class="language-plaintext highlighter-rouge">,reload</code>, <code class="language-plaintext highlighter-rouge">,edit</code> (which opens a definition in your
<code class="language-plaintext highlighter-rouge">$EDITOR</code>) and <code class="language-plaintext highlighter-rouge">,help</code> for the rest. The Clojure reader sees a comma as
whitespace, so they can’t clash with real code.</li>
  <li>Ctrl-C interrupts what’s running, and code that reads <code class="language-plaintext highlighter-rouge">*in*</code> gets its input,
something REPLy never quite got right.</li>
</ul>

<p>Without a server to connect to, <code class="language-plaintext highlighter-rouge">neorepl</code> starts one for the project, the way
CIDER’s jack-in does, with cider-nrepl on the classpath, and stops it when you
exit. It knows Leiningen, the Clojure CLI, babashka and shadow-cljs projects.</p>

<p>ClojureScript works too. Start a ClojureScript REPL in the session (through
shadow-cljs or piggieback, which jack-in adds for you) and neorepl follows
along: evaluation, completion, docs and the rest switch to ClojureScript, and
<code class="language-plaintext highlighter-rouge">:cljs/quit</code> takes you back.</p>

<h2 id="for-agents">For agents</h2>

<p>Coding agents are surprisingly good at Clojure, as long as they have a REPL
to talk to. Most of them drive CLIs much better than anything else, so neorepl
has a CLI that’s meant to be driven:</p>

<p><img src="/assets/images/neorepl-agent.gif" alt="neorepl's CLI" /></p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">neorepl eval</code> evaluates code on the project’s server. Calls share a
session, so <code class="language-plaintext highlighter-rouge">*ns*</code>, <code class="language-plaintext highlighter-rouge">*1</code> and <code class="language-plaintext highlighter-rouge">*e</code> carry over from one call to the next, and
<code class="language-plaintext highlighter-rouge">--session</code> keeps separate lines of work apart.</li>
  <li><code class="language-plaintext highlighter-rouge">--format json</code> (or <code class="language-plaintext highlighter-rouge">edn</code>) gives you the values, the output and the
namespace as data.</li>
  <li><code class="language-plaintext highlighter-rouge">--timeout</code> interrupts the evaluation before giving up, so an infinite loop
doesn’t take the server down with it, and <code class="language-plaintext highlighter-rouge">--max-output</code> keeps a stray
<code class="language-plaintext highlighter-rouge">(range)</code> from eating the agent’s context.</li>
  <li><code class="language-plaintext highlighter-rouge">neorepl check</code> finds unbalanced parens, brackets and braces without a
server. That’s the most common way for an agent (or a human) to break a
Clojure file.</li>
  <li><code class="language-plaintext highlighter-rouge">neorepl doc</code>, <code class="language-plaintext highlighter-rouge">source</code>, <code class="language-plaintext highlighter-rouge">apropos</code>, <code class="language-plaintext highlighter-rouge">complete</code>, <code class="language-plaintext highlighter-rouge">where</code>, <code class="language-plaintext highlighter-rouge">macroexpand</code> and
<code class="language-plaintext highlighter-rouge">reload</code> do what the REPL’s commands do.</li>
  <li><code class="language-plaintext highlighter-rouge">neorepl server</code> starts a server that stays up, for editors, agents and
<code class="language-plaintext highlighter-rouge">neorepl eval</code> to share.</li>
</ul>

<p>The exit codes are boring on purpose: 0 when all went well, 1 for an
evaluation error, 3 when there’s no server to talk to and 124 for a timeout
(like <code class="language-plaintext highlighter-rouge">timeout(1)</code>).</p>

<h2 id="playing-nicely-with-nrepl">Playing nicely with nREPL</h2>

<p>A new client is also a new way to break servers, or to be broken by them, and
the nREPL world has quite a few servers these days. Fortunately I’ve been
working on <a href="https://github.com/nrepl/proof">proof</a>, a compatibility suite for
nREPL, which made neorepl a good test subject.</p>

<p>proof checks both sides of the conversation. <code class="language-plaintext highlighter-rouge">proof proxy</code> sits between a
client and a server and grades everything the client sends: every request has
an id, <code class="language-plaintext highlighter-rouge">need-input</code> gets answered with <code class="language-plaintext highlighter-rouge">stdin</code> in the right session, and so on.
<code class="language-plaintext highlighter-rouge">proof serve</code> is a small nREPL server that can behave like other servers in
very specific ways - sending only the last value of the code like Basilisp and
jank, dropping stderr, having no <code class="language-plaintext highlighter-rouge">interrupt</code> op, splitting output into one
message per character, and so on. I ran neorepl’s REPL and CLI through both,
against nREPL, babashka, Basilisp and jank, and through every quirk <code class="language-plaintext highlighter-rouge">proof
serve</code> knows about.</p>

<p>neorepl came out of it in decent shape, but not unscathed:</p>

<ul>
  <li>Code read from stdin (<code class="language-plaintext highlighter-rouge">neorepl eval -</code>) that then read <code class="language-plaintext highlighter-rouge">*in*</code> itself crashed
neorepl and left the evaluation waiting on the server for input that was
never coming.</li>
  <li>neorepl’s errors could leave the shell’s prompt dangling at the end of a
line with servers that don’t end their error reports with a newline (hello,
jank!).</li>
  <li><code class="language-plaintext highlighter-rouge">neorepl eval</code> exited with 1 when it lost the server, which is the status
for code that failed.</li>
</ul>

<p>All of those are fixed in 0.1. It also turned up a couple of things that are
nREPL’s fault rather than neorepl’s: <code class="language-plaintext highlighter-rouge">need-input</code> and the end of an
interrupted evaluation can go to the wrong connection when a session is used
from more than one. Those will be fixed in nREPL itself.</p>

<p>It works the other way around too. proof now checks servers with the exact
requests neorepl sends while starting its REPL and evaluating code, next to
the ones CIDER, Calva, Conjure, vim-fireplace and REPLy send, so a server that
breaks neorepl finds out from proof, not from a bug report. nREPL, babashka,
Basilisp and jank all pass those checks today.</p>

<h2 id="making-the-neighbours-better">Making the neighbours better</h2>

<p>Building neorepl turned up a few problems elsewhere, which is one of my
favourite side effects of writing a new client. piggieback used to evaluate
only the first form you sent it and silently drop the rest, and it loaded the
ClojureScript compiler at startup even when nobody asked for a ClojureScript
REPL, which added almost half a second to the startup of every server with
ClojureScript on the classpath. The fixes for both
(<a href="https://github.com/nrepl/piggieback/pull/162">piggieback#162</a> and
<a href="https://github.com/nrepl/piggieback/pull/161">piggieback#161</a>) will be in the
next piggieback release. The Node.js
REPL in ClojureScript itself doesn’t cope well with interrupted evaluations,
and that one is next on my list.</p>

<h2 id="whats-next">What’s next</h2>

<p>0.1 is the foundation. Here’s what I’d like to tackle next:</p>

<ul>
  <li>a test runner with proper reports</li>
  <li>a full-screen inspector and stacktrace browser</li>
  <li>an MCP server and a ready-made skill for coding agents</li>
  <li>better Windows support</li>
</ul>

<h2 id="try-it">Try it</h2>

<p>The easiest way to install neorepl is <a href="https://github.com/babashka/bbin">bbin</a>:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>bbin <span class="nb">install </span>io.github.nrepl/neorepl
<span class="nv">$ </span><span class="nb">cd </span>my-project
<span class="nv">$ </span>neorepl
</code></pre></div></div>

<p>The <a href="https://github.com/nrepl/neorepl">README</a> has the details.</p>

<p>It’s early days, so I’m sure you’ll find rough edges. Please file issues,
and tell me what you’d like to see next. And if you’re one of the people who
kept <code class="language-plaintext highlighter-rouge">lein repl</code> and REPLy going over the years - thank you! neorepl wouldn’t
exist without you.</p>

<p>Keep hacking!</p>]]></content><author><name>Bozhidar Batsov</name></author><category term="posts" /><category term="Clojure" /><category term="nREPL" /><category term="neorepl" /><summary type="html"><![CDATA[For more than a decade the Clojure REPL most people saw in a terminal was REPLy, the one behind lein repl. It served us well, but it’s showing its age, and I kept wishing for a terminal REPL that felt like CIDER, without the Emacs part. So I finally built one. neorepl 0.1 is out today!]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://metaredux.com/assets/og-image.png" /><media:content medium="image" url="https://metaredux.com/assets/og-image.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">RuboCop 1.92: Agents Welcome</title><link href="https://metaredux.com/posts/2026/10/10/rubocop-1-92.html" rel="alternate" type="text/html" title="RuboCop 1.92: Agents Welcome" /><published>2026-10-10T09:30:00+00:00</published><updated>2026-10-10T09:30:00+00:00</updated><id>https://metaredux.com/posts/2026/10/10/rubocop-1-92</id><content type="html" xml:base="https://metaredux.com/posts/2026/10/10/rubocop-1-92.html"><![CDATA[<p><a href="https://github.com/rubocop/rubocop/releases/tag/v1.92.0">RuboCop 1.92</a> is out! Last time I talked about the long road to 2.0. This
time the theme is more down to earth, and probably closer to how a lot of you use
RuboCop today - AI coding agents.</p>

<p>These days plenty of RuboCop runs aren’t started by a person at all. Claude Code,
Codex, Cursor and friends run it after every batch of edits, read whatever it prints
and try to fix what it found. That’s a very different user from the one RuboCop was
built for. An agent has endless patience, but everything RuboCop prints ends up in its
context window, and it can’t glance at a terminal and tell what’s obviously noise. So
I spent most of this cycle making RuboCop a better citizen in that world.</p>

<!--more-->

<h2 id="output-an-agent-can-act-on">Output an agent can act on</h2>

<p><code class="language-plaintext highlighter-rouge">--format json</code> now tells you exactly what autocorrection would do with each offense.
Every correctable offense carries the edits, and whether a plain <code class="language-plaintext highlighter-rouge">rubocop -a</code> would
apply them:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"correction"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="nl">"safe"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"edits"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"start_line"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
      </span><span class="nl">"start_column"</span><span class="p">:</span><span class="w"> </span><span class="mi">6</span><span class="p">,</span><span class="w">
      </span><span class="nl">"last_line"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
      </span><span class="nl">"last_column"</span><span class="p">:</span><span class="w"> </span><span class="mi">12</span><span class="p">,</span><span class="w">
      </span><span class="nl">"begin_pos"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">,</span><span class="w">
      </span><span class="nl">"end_pos"</span><span class="p">:</span><span class="w"> </span><span class="mi">12</span><span class="p">,</span><span class="w">
      </span><span class="nl">"replacement"</span><span class="p">:</span><span class="w"> </span><span class="s2">"'hello'"</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>A cop that crashes on a file is now listed with that file’s results, instead of only
on stderr, where nothing that parses JSON ever looks. (And the <code class="language-plaintext highlighter-rouge">--parallel</code> notice no
longer goes to stdout, where it used to break the JSON output. Oops.)</p>

<p>There are also a few new options aimed squarely at agents (humans are welcome to use
them too):</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">--max-offenses-per-cop=N</code> caps how many offenses each cop reports. A legacy
codebase can easily have thousands of <code class="language-plaintext highlighter-rouge">Style/StringLiterals</code> offenses, and an agent
doesn’t need to read all of them to get the idea.</li>
  <li><code class="language-plaintext highlighter-rouge">--explain COP</code> prints what a cop checks, its good and bad examples, its
configuration as your project resolves it, and whether it autocorrects. Offense
messages are terse by design, and often that’s not enough to fix the code the way
the cop actually wants.</li>
  <li><code class="language-plaintext highlighter-rouge">--show-cops</code> and <code class="language-plaintext highlighter-rouge">--show-docs-url</code> now complain about cop names they don’t
recognize instead of silently ignoring them. Agents make up plausible-sounding cop
names more often than you’d think.</li>
</ul>

<h2 id="a-much-better-mcp-server">A much better MCP server</h2>

<p>Koichi added an experimental <a href="https://docs.rubocop.org/rubocop/usage/mcp.html">MCP server</a> back in RuboCop 1.85, and in this
release it got a serious overhaul:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">rubocop_inspection</code> answers in the same shape as <code class="language-plaintext highlighter-rouge">--format json</code> instead of as LSP
diagnostics. LSP diagnostics are great for editors, but an agent had to wade through
0-based lines and quick fixes it had no use for, and the results were about twice as
big.</li>
  <li>A new <code class="language-plaintext highlighter-rouge">rubocop_explain</code> tool does what <code class="language-plaintext highlighter-rouge">--explain</code> does.</li>
  <li>Inspection and autocorrection take <code class="language-plaintext highlighter-rouge">only</code>, <code class="language-plaintext highlighter-rouge">except</code> and <code class="language-plaintext highlighter-rouge">changed</code>, so an agent can
check just the files it touched, or just <code class="language-plaintext highlighter-rouge">Lint</code>.</li>
  <li>Autocorrection can preview its changes as a diff, reports the offenses it couldn’t
fix, and leaves alone the files it has nothing to correct in.</li>
  <li>Cops with <code class="language-plaintext highlighter-rouge">AutoCorrect: contextual</code>, like <code class="language-plaintext highlighter-rouge">Lint/UselessAssignment</code>, are held back by
default, since the code is probably mid-edit. An agent that’s done editing can ask
for them with <code class="language-plaintext highlighter-rouge">contextual: true</code>.</li>
  <li>A cop crashing on one file no longer fails the entire request.</li>
</ul>

<p>The server is still marked experimental, but it’s in a much better place now. If
you’re using it with your agent of choice, I’d love to hear how it goes.</p>

<h2 id="agents-and-rubocopdisable">Agents and <code class="language-plaintext highlighter-rouge">rubocop:disable</code></h2>

<p>There’s a new <a href="https://docs.rubocop.org/rubocop/usage/ai_agents.html">AI Agents</a> page in the docs that covers all of the above,
plus a few lines you can drop into your <code class="language-plaintext highlighter-rouge">AGENTS.md</code> or <code class="language-plaintext highlighter-rouge">CLAUDE.md</code> so the agent knows
how your project wants RuboCop run.</p>

<p>It also deals with my favorite agent “fix” - the <code class="language-plaintext highlighter-rouge"># rubocop:disable</code> that makes the
offense go away. Banning directives outright is too blunt, since some offenses really
are better left alone, but you can make the agent explain itself:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">Style/DisableCopsWithinSourceCodeDirective</span><span class="pi">:</span>
  <span class="na">Enabled</span><span class="pi">:</span> <span class="no">true</span>
  <span class="na">AllowWithReason</span><span class="pi">:</span> <span class="no">true</span>
</code></pre></div></div>

<p>Now every directive needs a <code class="language-plaintext highlighter-rouge">--</code> justification, and whoever reviews the change gets to
decide whether the reason holds up.</p>

<h2 id="sandboxes">Sandboxes</h2>

<p>More and more agents run in sandboxes that only allow access to the project
directory. Filesystem calls outside of it fail with <code class="language-plaintext highlighter-rouge">EPERM</code>, and RuboCop was mostly
prepared for <code class="language-plaintext highlighter-rouge">EACCES</code>, so a cache it couldn’t write to took the whole run down with
it. Now it just carries on without the cache. One more crash of that kind, in the
lookup of your target Ruby version, has a fix lined up for the next release.</p>

<h2 id="autocorrection-you-can-trust">Autocorrection you can trust</h2>

<p>This one isn’t specific to agents, but agents are the ones who suffer the most from
it. A person notices when <code class="language-plaintext highlighter-rouge">-a</code> produces garbage. An agent might happily build on top
of it. This release has a mountain of bug fixes, and most of them are about
autocorrection - dozens of infinite loops between cops, and an even longer list of
corrections that produced broken (or subtly different) code. Plenty of them came out
of a systematic audit of our autocorrections, and Koichi, <a href="https://github.com/viralpraxis">viralpraxis</a> and
<a href="https://github.com/Starlexxx">Starlexxx</a> fixed a staggering number of them.</p>

<h2 id="the-rest-of-the-release">The rest of the release</h2>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Lint/UselessAssignment</code> now catches a variable that a block keeps assigning but
nothing ever reads.</li>
  <li><code class="language-plaintext highlighter-rouge">Security/JSONLoad</code> now reports <code class="language-plaintext highlighter-rouge">JSON.load(src, create_additions: false)</code>. Before
json 2.17 the option is ignored in that position, so the “safe” call wasn’t safe at
all. <code class="language-plaintext highlighter-rouge">JSON.load(src, nil, create_additions: false)</code> works everywhere.</li>
  <li><code class="language-plaintext highlighter-rouge">Style/NegativeArrayIndex</code> is now marked as unsafe.</li>
  <li><code class="language-plaintext highlighter-rouge">--changed</code>, <code class="language-plaintext highlighter-rouge">--diff</code> and the SARIF formatter, all new in 1.91, got a round of fixes.</li>
  <li>RuboCop no longer loads <code class="language-plaintext highlighter-rouge">net/http</code> on every run, only when it fetches a remote
config.</li>
  <li><code class="language-plaintext highlighter-rouge">regexp_parser</code> 2.11.3 or newer is now required.</li>
</ul>

<p>The <a href="https://github.com/rubocop/rubocop/releases/tag/v1.92.0">release notes</a> have the full list.</p>

<h2 id="epilogue">Epilogue</h2>

<p>Agents are already a big part of how RuboCop gets used, and I think we’re only getting
started on adapting to that. Next on my list is <code class="language-plaintext highlighter-rouge">--changed-lines</code>, so an agent (or a
person) can focus on the offenses in the lines it actually changed. If you’re running
RuboCop through an agent and something about it annoys you, <a href="https://github.com/rubocop/rubocop/issues">tell us</a>.</p>

<p>One more thing before I go - <a href="https://rubocop.org">rubocop.org</a> got completely reworked recently.
Most of what it shows now comes straight out of RuboCop itself, there’s an index of
every cop, and there’s a <a href="https://rubocop.org/playground/">playground</a> that runs RuboCop right in your
browser, courtesy of ruby.wasm. Go check it out and take the playground for a spin!
Feedback on the new design and features is most welcome - just
<a href="https://github.com/rubocop/rubocop.org/issues">open an issue</a> if something looks off or you’re missing something.</p>

<p>Big thanks to everyone who contributed to this release! Koichi, viralpraxis and
Starlexxx carried the bug-fixing once again, and <a href="https://github.com/bquorning">bquorning</a> reported the sandbox
crash that got me looking into the rest of them.</p>

<p>That’s all I have for you today. Keep hacking!</p>]]></content><author><name>Bozhidar Batsov</name></author><category term="posts" /><category term="Ruby" /><category term="RuboCop" /><summary type="html"><![CDATA[RuboCop 1.92 is out! Last time I talked about the long road to 2.0. This time the theme is more down to earth, and probably closer to how a lot of you use RuboCop today - AI coding agents. These days plenty of RuboCop runs aren’t started by a person at all. Claude Code, Codex, Cursor and friends run it after every batch of edits, read whatever it prints and try to fix what it found. That’s a very different user from the one RuboCop was built for. An agent has endless patience, but everything RuboCop prints ends up in its context window, and it can’t glance at a terminal and tell what’s obviously noise. So I spent most of this cycle making RuboCop a better citizen in that world.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://metaredux.com/assets/og-image.png" /><media:content medium="image" url="https://metaredux.com/assets/og-image.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">RuboCop 1.91: The Journey to 2.0 Begins</title><link href="https://metaredux.com/posts/2026/09/11/rubocop-1-91.html" rel="alternate" type="text/html" title="RuboCop 1.91: The Journey to 2.0 Begins" /><published>2026-09-11T05:00:00+00:00</published><updated>2026-09-11T05:00:00+00:00</updated><id>https://metaredux.com/posts/2026/09/11/rubocop-1-91</id><content type="html" xml:base="https://metaredux.com/posts/2026/09/11/rubocop-1-91.html"><![CDATA[<p><a href="https://github.com/rubocop/rubocop/releases/tag/v1.91.0">RuboCop 1.91</a> is out! It has the usual mix of new cops, new options and a
small mountain of bug fixes, but the feature I want to talk about today is a single
flag - <code class="language-plaintext highlighter-rouge">--preview</code>. It’s the first real step on our (long) journey to RuboCop 2.0.</p>

<!--more-->

<h2 id="since-the-beginning-of-time">Since the beginning of time…</h2>

<p>…certain people have been unhappy with RuboCop’s defaults. I wrote almost exactly
that sentence <a href="/posts/2020/03/27/adjusting-rubocop-s-defaults.html">six years ago</a>,
right before RuboCop 1.0, when I ran a survey to work out which of our defaults were
wrong. Some good came out of it - that’s how <code class="language-plaintext highlighter-rouge">Layout/LineLength</code> ended up at 120 -
but a survey has an obvious weakness. It asks people what they prefer, and people
are terrible at reporting what they actually do.</p>

<p>So this time I went looking for evidence instead of opinions. I built
<a href="https://github.com/rubocop/rubocop/pull/15362">a small tool</a> that grabs the 500 most-starred Ruby repos on GitHub,
resolves whatever <code class="language-plaintext highlighter-rouge">.rubocop.yml</code> they ship (following <code class="language-plaintext highlighter-rouge">inherit_from</code> chains and
popular <code class="language-plaintext highlighter-rouge">inherit_gem</code> bases like <code class="language-plaintext highlighter-rouge">standard</code> and <code class="language-plaintext highlighter-rouge">rubocop-rails-omakase</code>), and tallies
what they change about our defaults. 233 of them have a config, and
<a href="https://github.com/rubocop/rubocop/issues/15363">the results</a> were interesting in various ways:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Style/Documentation</code> is turned off outright by 53% of them. It’s the most disabled
cop by a wide margin.</li>
  <li>Seven of the ten most disabled cops are <code class="language-plaintext highlighter-rouge">Metrics</code> cops.</li>
  <li>Our <code class="language-plaintext highlighter-rouge">Metrics/MethodLength</code> default is 10; the median project that touches it sets
    <ol>
      <li><code class="language-plaintext highlighter-rouge">AbcSize</code> goes from 17 to 28. <code class="language-plaintext highlighter-rouge">ClassLength</code> goes from 100 to 300.</li>
    </ol>
  </li>
  <li>91% of the projects that set <code class="language-plaintext highlighter-rouge">Style/StringLiterals</code> pick <code class="language-plaintext highlighter-rouge">double_quotes</code> - and all
three of the big shared configs people inherit wholesale already default to it.</li>
</ul>

<p><strong>Note:</strong> people write a line of config only when they <em>disagree</em> with us, so happy
users are invisible here. This shows how far opinionated projects drift from us, not
how everyone feels. Evidence, not proof. Still - when a third of the most visible
Ruby projects out there disable a cop, that cop isn’t teaching anyone anything. It’s
just noise people have learned to mute.</p>

<h2 id="knowing-is-the-easy-part">Knowing is the easy part</h2>

<p>Knowing which defaults are “wrong” and being able to <em>change</em> them are two very
different things. We follow SemVer (semi-)religiously, and changing a default - a
cop’s <code class="language-plaintext highlighter-rouge">Enabled</code> state, a <code class="language-plaintext highlighter-rouge">Max</code> threshold, an <code class="language-plaintext highlighter-rouge">EnforcedStyle</code> - is a breaking change
that has to wait for a major release. That’s exactly the promise we made in
<a href="/posts/2020/10/21/rubocop-1-0.html">RuboCop 1.0</a>, and I have no intention of
walking it back.</p>

<p>Same story for a cop that starts reporting a case it used to miss, or corrects one
differently. Those aren’t new cops, so <code class="language-plaintext highlighter-rouge">pending</code> is no help - they change results for
everyone who already has the cop enabled.</p>

<p>Which leaves us with a lovely little trap. Everything interesting piles up in a queue
waiting for a major release; the major release gets scarier the longer the queue
grows; so it gets postponed; so the queue grows some more. You can see where this
ends. In the 1.0 post I wrote that RuboCop 2.0 would “probably arrive in less than 8
years”, and we’re six years in. I’d very much like to beat my own estimate. I really
do!</p>

<h2 id="enter-preview-mode">Enter preview (mode)</h2>

<p><code class="language-plaintext highlighter-rouge">Preview</code> mode is our way out of that trap. It’s an opt-in channel where the next
major release’s behavior lives until that release ships:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">AllCops</span><span class="pi">:</span>
  <span class="na">Preview</span><span class="pi">:</span> <span class="no">true</span>
</code></pre></div></div>

<p>Or, for a single run:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>rubocop <span class="nt">--preview</span>
</code></pre></div></div>

<p>It gates three kinds of things. A cop’s entry in the default configuration can now
carry a <code class="language-plaintext highlighter-rouge">Preview</code> section holding the defaults it’s expected to adopt in 2.0:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">Style/Documentation</span><span class="pi">:</span>
  <span class="na">Enabled</span><span class="pi">:</span> <span class="no">true</span>
  <span class="na">Preview</span><span class="pi">:</span>
    <span class="na">Enabled</span><span class="pi">:</span> <span class="no">false</span>
</code></pre></div></div>

<p>A cop can also hide an unstable <em>behavior</em> change behind a <code class="language-plaintext highlighter-rouge">preview?</code> check and ship
it in a minor release. And a cop that’s too speculative to even be <code class="language-plaintext highlighter-rouge">pending</code> can ship
as <code class="language-plaintext highlighter-rouge">Enabled: preview</code>. In all three cases your own configuration still wins over the
default one, exactly as it always has.</p>

<p>The line between <code class="language-plaintext highlighter-rouge">preview</code> and <code class="language-plaintext highlighter-rouge">pending</code> is worth keeping straight, as both mean
“not on by default”. When a new cop gets introduced it’s usually <code class="language-plaintext highlighter-rouge">pending</code> (and
sometimes disabled) - a marker for a pending decision (from you or us). If we’re asking you to
help us find out if something is a good idea, it’s <code class="language-plaintext highlighter-rouge">preview</code>.</p>

<p>I know that’s a bit confusing right now and perhaps down the road <code class="language-plaintext highlighter-rouge">preview</code> will
replace <code class="language-plaintext highlighter-rouge">pending</code> completely for simplicity’s sake. That’s to be determined at this
point. Some ideas and thoughts on this front are most welcome!</p>

<p>One thing to be clear about: preview behavior is unstable and can change or be
withdrawn in any release. That’s the whole point. It makes trying something out cheap
for us, and telling us we got it wrong cheap for you.</p>

<p>I’d love to tell you the idea was original, but it wasn’t. It came out of an
afternoon spent looking at how other linters handle this problem, and they had all
landed in much the same place. <a href="https://docs.astral.sh/ruff/preview/">Ruff</a> has had a <code class="language-plaintext highlighter-rouge">preview</code> mode for a while
now - one global switch gating both preview-only rules and changed behavior inside
existing ones - and we copied that model wholesale. <a href="https://doc.rust-lang.org/clippy/lints.html">Clippy</a> parks its
unfinished and contested lints in a <code class="language-plaintext highlighter-rouge">nursery</code> group that stays off until they’ve
earned their keep. Both admit the same thing: if the only way to try something is to
ship it to everyone at once, you’ll never try anything.</p>

<h2 id="whats-in-the-box-today">What’s in the box today</h2>

<p>1.91 ships the mechanism <em>and</em> a first batch of proposed 2.0 defaults, taken straight
from that corpus data. Flip preview on today and you get:</p>

<ul>
  <li>The entire <code class="language-plaintext highlighter-rouge">Metrics</code> department off. Not because measuring complexity is a bad idea,
but because our thresholds are so far off that people disable the cops rather than
argue with them. I’d rather ship no opinion than a bad one.</li>
  <li>Roughly two dozen contested <code class="language-plaintext highlighter-rouge">Style</code>, <code class="language-plaintext highlighter-rouge">Naming</code> and <code class="language-plaintext highlighter-rouge">Layout</code> cops off -
<code class="language-plaintext highlighter-rouge">Style/IfUnlessModifier</code>, <code class="language-plaintext highlighter-rouge">Style/GuardClause</code>, <code class="language-plaintext highlighter-rouge">Style/ClassAndModuleChildren</code>,
<code class="language-plaintext highlighter-rouge">Style/DoubleNegation</code>, <code class="language-plaintext highlighter-rouge">Style/FrozenStringLiteralComment</code>, <code class="language-plaintext highlighter-rouge">Style/BlockDelimiters</code>
and friends. They’re all near the top of the disable leaderboard.</li>
  <li><code class="language-plaintext highlighter-rouge">Style/Documentation</code> off, obviously.</li>
  <li>The community’s <code class="language-plaintext highlighter-rouge">EnforcedStyle</code> wherever it differs from ours: <code class="language-plaintext highlighter-rouge">double_quotes</code> for
<code class="language-plaintext highlighter-rouge">Style/StringLiterals</code>, <code class="language-plaintext highlighter-rouge">brackets</code> for <code class="language-plaintext highlighter-rouge">Style/SymbolArray</code> and <code class="language-plaintext highlighter-rouge">Style/WordArray</code>,
and <code class="language-plaintext highlighter-rouge">consistent</code>/<code class="language-plaintext highlighter-rouge">indented</code>/<code class="language-plaintext highlighter-rouge">with_fixed_indentation</code> across the multiline
indentation family.</li>
  <li><code class="language-plaintext highlighter-rouge">AllCops: FailLevel: warning</code>, so style offenses get reported without failing your
build. More on this one in a moment.</li>
  <li><code class="language-plaintext highlighter-rouge">Exclude</code> merging with the default configuration and with inherited files instead of
replacing them - the fix for <a href="https://github.com/rubocop/rubocop/issues/9325">#9325</a>, one of our oldest configuration
papercuts.</li>
</ul>

<p>That’s 34 cops switched off and 17 defaults adjusted. A start, not a destination.</p>

<h2 id="a-less-prescriptive-rubocop">A less prescriptive RuboCop</h2>

<p>I want RuboCop 2.0 to ship a much smaller and much less prescriptive default
configuration than 1.x does.</p>

<p>This isn’t a new idea for me. Back in the <a href="/posts/2022/04/21/rubocop-turns-10.html">10th anniversary
post</a> I admitted that “we might have gone
overboard at times in our desire to improve Ruby codebases”, and I listed bundling
cops into presets as one of the things I wanted for 2.0, to address “the common
complaint that RuboCop checks for way too many things out of the box”. Four years
later that complaint hasn’t gone anywhere, and now I’ve got the numbers to back it up.</p>

<p>There’s a slightly embarrassing part to this story too. It wasn’t until I was digging
through that data that it hit me how many cops we ship these days. 613. I’ve been
maintaining this project for fourteen years and that number genuinely shocked me.
Sure, I knew it was a lot - I’d just never looked at the figure, and once you have,
it’s hard to unsee. That’s what got me focused on making the default configuration
leaner.</p>

<p>RuboCop grew up as the “enforcement” arm of the <a href="https://rubystyle.guide">Ruby Style Guide</a>, and
for many years “is it in the style guide?” was a good enough answer to “should it be
on by default?”. I still stand firmly behind the style guide. But the tool has
outgrown that framing - that’s an awful lot of opinions to hand someone on their first
day, and the data says most people don’t want most of them. Out of the box RuboCop
should catch real problems and stay out of your way on matters of taste. If you want
the full sermon, you should have to ask for it.</p>

<p>Sure, this sounds a lot like <a href="https://github.com/standardrb/standard">Standard</a>, and I have no problem admitting
they got there before we did. The popularity of Standard and <code class="language-plaintext highlighter-rouge">rubocop-rails-omakase</code>
was the community telling us something, and we took our sweet time listening. The
difference is that RuboCop will stay exactly as configurable as it’s always been. I’m
not taking away any knobs, I’m changing which ones start in the “on” position.<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> If
your project loves our current defaults, you’ll be able to keep every last one of them.</p>

<p>Preview isn’t the only thing in motion here, either. The presets work I promised in
that anniversary post is moving again - <a href="https://github.com/rubocop/rubocop/issues/14005">#14005</a> has the discussion and
<a href="https://github.com/rubocop/rubocop/pull/14994">#14994</a> a first prototype - and I see the two as related rather than
competing. Preview asks “is this particular default wrong?”. Presets ask the broader
“how many cops should be running at all?”, by letting you pick a bundle - something
essential, something recommended, the full style guide - instead of arguing over
several hundred separate decisions. And the preview feedback feeds straight into that:
every cop you tell me you don’t miss is a cop that belongs in a stricter tier, and
every one you yell at me about belongs in the baseline. I’d like to land both for 2.0.</p>

<p>The severity work in this release is part of the same push. A cop’s default severity
now comes from its department, so <code class="language-plaintext highlighter-rouge">Security</code> cops report as <code class="language-plaintext highlighter-rouge">warning</code> and <code class="language-plaintext highlighter-rouge">Metrics</code>
cops as <code class="language-plaintext highlighter-rouge">refactor</code>. Combined with the new <code class="language-plaintext highlighter-rouge">AllCops: FailLevel</code> - the configuration
equivalent of <code class="language-plaintext highlighter-rouge">--fail-level</code>, which somehow we never had - you finally get a better
dial than “everything fails the build equally”:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">AllCops</span><span class="pi">:</span>
  <span class="na">FailLevel</span><span class="pi">:</span> <span class="s">warning</span>
</code></pre></div></div>

<p>With that, only <code class="language-plaintext highlighter-rouge">Lint</code> and <code class="language-plaintext highlighter-rouge">Security</code> offenses (plus anything you’ve bumped yourself)
fail the run. Everything else still gets printed, it just doesn’t paint your CI red.</p>

<h2 id="the-rest-of-the-release">The rest of the release</h2>

<p>Preview stole the show, but there’s plenty else in here - and a few of these came out
of that same survey of the competition:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">--diff</code> prints a unified diff of what autocorrection <em>would</em> change, without
touching a single file. This is the CI check I’ve wanted for ages - the build fails
with the patch that would fix it sitting right there in the log, and
<code class="language-plaintext highlighter-rouge">rubocop --diff --format quiet | git apply</code> gets you that patch locally.</li>
  <li><code class="language-plaintext highlighter-rouge">--changed</code> inspects only the files git says changed, defaulting to a diff against
<code class="language-plaintext highlighter-rouge">HEAD</code> (any revision works, and untracked files count). It pairs beautifully with
<code class="language-plaintext highlighter-rouge">--diff</code>.</li>
  <li>There’s a SARIF formatter now, so <code class="language-plaintext highlighter-rouge">rubocop --format sarif --out rubocop.sarif</code> feeds
straight into GitHub code scanning and your offenses show up annotated on the pull
request diff.</li>
  <li><code class="language-plaintext highlighter-rouge">Lint/MisplacedMagicComment</code> is a new cop that flags magic comments Ruby silently
ignores - an <code class="language-plaintext highlighter-rouge"># encoding:</code> comment that isn’t on the first line, a
<code class="language-plaintext highlighter-rouge"># frozen_string_literal: true</code> that drifted below a <code class="language-plaintext highlighter-rouge">require</code>, a magic comment that
pushed a shebang off line one. Quietly doing nothing is the worst failure mode a
magic comment can have.</li>
  <li>Two new directives. <code class="language-plaintext highlighter-rouge"># rubocop:enable-next</code> re-enables a cop for exactly one
statement, with no closing directive to maintain. <code class="language-plaintext highlighter-rouge"># rubocop:next</code> covers the same
statement scope but takes <code class="language-plaintext highlighter-rouge">push</code>-style <code class="language-plaintext highlighter-rouge">+</code>/<code class="language-plaintext highlighter-rouge">-</code> arguments, for the rare case that
needs toggles in both directions at once. <code class="language-plaintext highlighter-rouge">Style/DirectiveScope</code> will convert your
existing pairs to the tighter forms.</li>
  <li><code class="language-plaintext highlighter-rouge">Lint/CopDirectiveSyntax</code> is enabled by default now, so a directive that silently
suppresses nothing gets reported instead of fooling you for years. It absorbed
<code class="language-plaintext highlighter-rouge">Style/DoubleCopDisableDirective</code>, which is gone.</li>
  <li><code class="language-plaintext highlighter-rouge">Style/DisableCopsWithinSourceCodeDirective</code> gained an <code class="language-plaintext highlighter-rouge">AllowedDirectives</code> option, so
you can ban hand-written <code class="language-plaintext highlighter-rouge">disable</code> comments while still letting generated
<code class="language-plaintext highlighter-rouge">rubocop:todo</code> entries through. Its cop lists match department names too now.</li>
  <li>And a mountain of bug fixes - a long parade of infinite loops between cops squashed,
more crashes and bad autocorrections than I care to admit to, and two result cache
bugs, one of which could serve one file’s cached results for another file. The
<a href="https://github.com/rubocop/rubocop/releases/tag/v1.91.0">release notes</a> have the full list.</li>
</ul>

<h2 id="epilogue">Epilogue</h2>

<p>The most useful thing you can do right now is run <code class="language-plaintext highlighter-rouge">rubocop --preview</code> on your codebase
and tell us what you think. Which of those disabled cops do you actually miss? Which
<code class="language-plaintext highlighter-rouge">EnforcedStyle</code> did we get wrong? What did we leave switched on that should have gone?
That’s the whole reason preview exists - I’d much rather find out now than on the day
2.0 ships. <a href="https://github.com/rubocop/rubocop/issues">Open an issue</a>, comment on <a href="https://github.com/rubocop/rubocop/issues/15363">the defaults
discussion</a>, or just yell about it on the internet. I’ll find it.</p>

<p>Big thanks to everyone who contributed to this release! <a href="https://github.com/viralpraxis">viralpraxis</a> and
<a href="https://github.com/Starlexxx">Starlexxx</a> in particular did a huge amount of the bug-fixing this cycle, and Koichi
keeps being Koichi. And thanks to everyone who’s been arguing with our defaults all
these years - turns out you were mostly right.</p>

<p>That’s all I have for you today. Keep hacking!</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>Famous last words, I’m well aware. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Bozhidar Batsov</name></author><category term="posts" /><category term="Ruby" /><category term="RuboCop" /><summary type="html"><![CDATA[RuboCop 1.91 is out! It has the usual mix of new cops, new options and a small mountain of bug fixes, but the feature I want to talk about today is a single flag - --preview. It’s the first real step on our (long) journey to RuboCop 2.0.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://metaredux.com/assets/og-image.png" /><media:content medium="image" url="https://metaredux.com/assets/og-image.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Clojurists Together Update: July and August 2026</title><link href="https://metaredux.com/posts/2026/09/10/clojurists-together-update-july-august-2026.html" rel="alternate" type="text/html" title="Clojurists Together Update: July and August 2026" /><published>2026-09-10T09:30:00+00:00</published><updated>2026-09-10T09:30:00+00:00</updated><id>https://metaredux.com/posts/2026/09/10/clojurists-together-update-july-august-2026</id><content type="html" xml:base="https://metaredux.com/posts/2026/09/10/clojurists-together-update-july-august-2026.html"><![CDATA[<p>Time for another bi-monthly update on the work that <a href="https://www.clojuriststogether.org/">Clojurists Together</a> are funding this year - my
maintenance of <a href="https://nrepl.org">nREPL</a>, <a href="https://cider.mx">CIDER</a> and friends. I published the <a href="/posts/2026/07/05/clojurists-together-update-may-june-2026.html">previous one</a> as a blog post for the first time
and the feedback was good enough that I’ll keep doing it.</p>

<p>Last time I said that I had plucked most of the low-hanging fruit and that the next two months were unlikely to be as productive. Well, I was wrong.
CIDER 2.0 finally shipped, and once it was out the door I used the momentum to sweep through pretty much every corner of the nREPL/CIDER ecosystem.
A few long-neglected projects got proper releases, and nREPL got a couple of brand new implementations in languages I play on the side from time to time.</p>

<!--more-->

<p>The big highlights from my perspective:</p>

<ul>
  <li>CIDER 2.0 (“Terceira”) is out, followed by 2.0.1, and 2.1 is taking shape on master</li>
  <li><a href="https://github.com/clojure-emacs/clj-refactor.el">clj-refactor</a> 4.0 is out</li>
  <li><a href="https://github.com/clojure-emacs/sayid">Sayid</a> went from 0.4 to 0.8 in the span of three weeks</li>
  <li><a href="https://github.com/nrepl/drawbridge">Drawbridge</a>, nREPL’s HTTP transport, got its first meaningful release in years</li>
  <li>nREPL went polyglot: <a href="https://github.com/nrepl/nrepl-beam">nREPL servers for Erlang and Elixir</a> and <a href="https://github.com/nrepl/mezcaml">an OCaml client</a></li>
  <li><a href="https://github.com/clojure-emacs/clj-suitable">clj-suitable</a> 0.7 and 0.8 closed most of the gap between ClojureScript and Clojure completion</li>
  <li>A lot of work landed on nREPL’s master (TLS hardening, URL-based connections, docs) and a new release is right around the corner</li>
</ul>

<p>Below you’ll find more details about the work I did, project by project.</p>

<h2 id="cider">CIDER</h2>

<p><a href="https://github.com/clojure-emacs/cider/releases/tag/v2.0.0">CIDER 2.0 (“Terceira”)</a> landed on July 15, right on the schedule I had
announced in the <a href="/posts/2026/06/30/cider-2-0-is-brewing.html">preview post</a>. For once in my life I was actually on time!
The big themes were covered there and in the <a href="/posts/2026/07/15/cider-2-0.html">release announcement</a> (transient menus, inline macro stepping, call-graph browsers,
source-based find-references, the tracing and tap buffers, rich content in the REPL), so here’s just what changed between
the preview and the final release:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">cider-doctor</code>, which checks your Emacs setup and the active nREPL session for common problems and produces a report you can paste in a bug report</li>
  <li>an <code class="language-plaintext highlighter-rouge">orchard</code> value for <code class="language-plaintext highlighter-rouge">cider-print-fn</code>, selecting cider-nrepl’s much faster <code class="language-plaintext highlighter-rouge">orchard.pp</code> pretty-printer</li>
  <li>SSH tunnels now forward a free local port, so remote REPLs sharing a port no longer collide on localhost</li>
  <li><code class="language-plaintext highlighter-rouge">C-c C-d</code> at the stdin prompt sends end-of-input, and stdin is routed to the exact connection that asked for it</li>
  <li>a long tail of nREPL client fixes: a slow memory leak on the eldoc/completion path, <code class="language-plaintext highlighter-rouge">nrepl-dict-merge</code> mutating a shared literal, notifications treated as format strings</li>
</ul>

<p><a href="https://github.com/clojure-emacs/cider/releases/tag/v2.0.1">CIDER 2.0.1</a> followed a week later with fixes for the problems early
adopters ran into: evaluation in a dependency’s source buffer erroring with “No linked CIDER sessions” (in several variants),
<code class="language-plaintext highlighter-rouge">cider-enlighten-mode</code> never lighting anything up (a 1.22 regression), the macroexpansion commands refusing to expand
<code class="language-plaintext highlighter-rouge">let</code>/<code class="language-plaintext highlighter-rouge">fn</code>/<code class="language-plaintext highlighter-rouge">loop</code>, and <code class="language-plaintext highlighter-rouge">load-file</code> potentially freezing Emacs on a huge result. Nothing dramatic, but I’m glad people were quick to report those.</p>

<p>After that master (the future CIDER 2.1) kept moving at a steady pace. A few of the things that landed there:</p>

<ul>
  <li>CIDER’s dynamic font-locking (REPL-defined macros, functions, deprecated/instrumented/traced symbols) now works better in <code class="language-plaintext highlighter-rouge">clojure-ts-mode</code> buffers via tree-sitter. Previously it worked “officially” only under <code class="language-plaintext highlighter-rouge">clojure-mode</code>. The debugging reader tags are highlighted there too.</li>
  <li>A new <code class="language-plaintext highlighter-rouge">cider-preferred-clojure-mode</code> controls which mode CIDER uses to font-lock the code it renders - REPL results, doc examples, overlays and its own display buffers. <code class="language-plaintext highlighter-rouge">clojure-ts-mode</code> is finally a first-class citizen in CIDER.</li>
  <li>Symbol prompts can go through <code class="language-plaintext highlighter-rouge">completing-read</code> (so Vertico/Ivy/Helm kick in) and completion annotations render as an aligned type/namespace column in Corfu, Vertico and the built-in <code class="language-plaintext highlighter-rouge">*Completions*</code>. More on this in <a href="/posts/2026/07/25/modernizing-cider-completion.html">Modernizing CIDER’s Completion</a>.</li>
  <li>Connecting got smarter. Container-published nREPL ports are resolved for <code class="language-plaintext highlighter-rouge">/docker:</code> and <code class="language-plaintext highlighter-rouge">/podman:</code> buffers, <code class="language-plaintext highlighter-rouge">lein trampoline</code> REPLs are detected, <code class="language-plaintext highlighter-rouge">.nrepl-port</code> files are no longer discarded on systems without <code class="language-plaintext highlighter-rouge">lsof</code>, and there’s a new <a href="https://docs.cider.mx/cider/basics/up_and_running.html#how-cider-finds-ports">“How CIDER Finds Ports”</a> section in the manual.</li>
  <li>Every form command got an “at point” variant (inspect, pprint, macroexpand, format, insert in REPL), there’s a <code class="language-plaintext highlighter-rouge">cider-inspect-menu</code> listing every way to start an inspection, and the contents of <code class="language-plaintext highlighter-rouge">comment</code> forms are treated as top level by the whole defun command family.</li>
  <li>Stray output from long-lived background processes (say, a <code class="language-plaintext highlighter-rouge">core.async</code> go-loop still printing under a finished eval’s id) is now routed to the REPL instead of being dropped with a warning.</li>
</ul>

<p>One more thing. I shipped “smarter form targeting” on master - the evaluation commands resolving the form from where the cursor
actually is, rather than the form before it - <a href="/posts/2026/08/26/smarter-form-targeting-is-coming-to-cider.html">wrote about it</a>,
got a lot of feedback, and <a href="/posts/2026/08/29/smarter-form-targeting-is-not-coming-to-cider.html">reverted it</a> a few days later.
CIDER 2.1 will keep the classic Emacs semantics. Fifteen years in, the existing behaviour is the contract, not an implementation detail I get to tidy up.
The detour wasn’t wasted, though: it surfaced a bug where the text of a line comment was treated as code,
and the “at point” family of commands is a direct result of it.</p>

<h2 id="cider-nrepl">cider-nrepl</h2>

<p>Three <a href="https://github.com/clojure-emacs/cider-nrepl">cider-nrepl</a> releases in July, wrapping up the tools.deps migration and driving the CIDER 2.0 launch:</p>

<ul>
  <li><a href="https://github.com/clojure-emacs/cider-nrepl/releases/tag/v0.62.0">cider-nrepl 0.62.0</a> finalized the Leiningen to tools.deps migration, simplified deferred middleware loading, documented the op response keys (with a test verifying the descriptor contract) and shipped the hardened content-type and slurp middleware that made rich content safe to enable by default.</li>
  <li><a href="https://github.com/clojure-emacs/cider-nrepl/releases/tag/v0.62.1">cider-nrepl 0.62.1</a> fixed a whole cluster of debugger bugs. Record literals no longer get downgraded to plain maps by instrumentation, <code class="language-plaintext highlighter-rouge">deftype</code>/<code class="language-plaintext highlighter-rouge">defrecord</code> method bodies are skipped (goodbye <code class="language-plaintext highlighter-rouge">Unable to resolve symbol: STATE__</code>), and enlightening <code class="language-plaintext highlighter-rouge">deftest</code> bodies works again.</li>
  <li><a href="https://github.com/clojure-emacs/cider-nrepl/releases/tag/v0.62.2">cider-nrepl 0.62.2</a> pruned trace and tap subscriptions with dead transports (a dead subscriber used to break every traced evaluation), stopped the debugger from shadowing enlighten’s evaluator, brought the docs back in sync with the code and added a <a href="https://docs.cider.mx/cider-nrepl/tool-authors/index.html">section for tool authors</a>.</li>
</ul>

<h2 id="orchard">Orchard</h2>

<p><a href="https://github.com/clojure-emacs/orchard/releases/tag/v0.44.0">Orchard 0.44.0</a> shipped on July 4, mostly thanks to Sashko’s inspector work (a <code class="language-plaintext highlighter-rouge">replace</code> command,
truncated table columns, <code class="language-plaintext highlighter-rouge">ARef</code> contents rendered fully). My part was a round of tests for the less covered namespaces and, later on master,
a fix for <code class="language-plaintext highlighter-rouge">orchard.print</code> ignoring custom <code class="language-plaintext highlighter-rouge">print-method</code> implementations for records and collections. Thanks, Sashko!</p>

<h2 id="clj-refactor-40">clj-refactor 4.0</h2>

<p><a href="https://github.com/clojure-emacs/clj-refactor.el/releases/tag/v4.0.0">clj-refactor.el 4.0</a> is the release I had been promising for a few cycles.
It requires Emacs 28.1+ and CIDER 2.0+, and it’s a big one:</p>

<ul>
  <li>project-wide refactorings (rename symbol, change signature, inline symbol) now show a diff preview before touching disk, and <code class="language-plaintext highlighter-rouge">cljr-undo-last-refactoring</code> reverts the last one in a single step</li>
  <li>the slow refactorings run asynchronously, so Emacs no longer freezes while the middleware analyzes the project</li>
  <li><code class="language-plaintext highlighter-rouge">cljr-change-function-signature</code> can add and remove parameters and handles multi-arity functions</li>
  <li>a <code class="language-plaintext highlighter-rouge">clj-refactor-menu</code> transient replaces the hydra menus, and the <code class="language-plaintext highlighter-rouge">multiple-cursors</code>, <code class="language-plaintext highlighter-rouge">hydra</code> and <code class="language-plaintext highlighter-rouge">inflections</code> dependencies are gone</li>
  <li>many commands degrade gracefully without a REPL (<code class="language-plaintext highlighter-rouge">cljr-clean-ns</code>, <code class="language-plaintext highlighter-rouge">cljr-slash</code>, <code class="language-plaintext highlighter-rouge">cljr-add-missing-libspec</code>, <code class="language-plaintext highlighter-rouge">cljr-remove-let</code>, <code class="language-plaintext highlighter-rouge">cljr-promote-function</code>)</li>
  <li><code class="language-plaintext highlighter-rouge">cljr-slash</code> can add and hotload a missing library, artifact lists are cached, and the namespaced refactor-nrepl ops are used when available</li>
</ul>

<p>I still think the long-term home for the most useful bits is CIDER and clojure-mode, but at least the project is in good shape while I figure that out.
There’s a bit more in the <a href="/posts/2026/07/16/clj-refactor-4-0.html">release post</a>.</p>

<h2 id="clj-suitable">clj-suitable</h2>

<p><a href="https://github.com/clojure-emacs/clj-suitable">clj-suitable</a>, the ClojureScript completion backend, was another project that had been coasting for years:</p>

<ul>
  <li><a href="https://github.com/clojure-emacs/clj-suitable/releases/tag/0.7.0">clj-suitable 0.7.0</a> adapted to Piggieback 0.7’s delegating repl-env, modernized every dependency, replaced the Leiningen build with tools.build, moved CI to GitHub Actions and added a shadow-cljs integration test over a real Node runtime.</li>
  <li><a href="https://github.com/clojure-emacs/clj-suitable/releases/tag/0.8.0">clj-suitable 0.8.0</a> brought the static completion much closer to compliment: fuzzy matching (<code class="language-plaintext highlighter-rouge">pr-fn</code> completes <code class="language-plaintext highlighter-rouge">print-function</code>), compliment-style ranking, completion of local bindings (destructuring included) and of referred vars inside <code class="language-plaintext highlighter-rouge">:refer</code> vectors. It also fixed the REPL’s <code class="language-plaintext highlighter-rouge">*1</code>/<code class="language-plaintext highlighter-rouge">*2</code>/<code class="language-plaintext highlighter-rouge">*3</code> getting clobbered by completions and a few long-standing shadow-cljs and Node.js issues.</li>
</ul>

<p>ClojureScript users - I’d love to hear how the new completion feels in practice.</p>

<h2 id="sayid">Sayid</h2>

<p>The <a href="/posts/2026/07/01/sayid-redux.html">Sayid revival</a> continued at a brisk pace, with five releases between July 1 and July 17:</p>

<ul>
  <li><a href="https://github.com/clojure-emacs/sayid/blob/master/CHANGELOG.md#040---2026-07-01">Sayid 0.4.0</a> dropped the <code class="language-plaintext highlighter-rouge">com.billpiel</code> namespace prefix, added data-returning variants of the workspace and query ops, and introduced a client-rendered, foldable tree view of the recorded call tree built on CIDER’s <code class="language-plaintext highlighter-rouge">cider-tree-view</code>.</li>
  <li><a href="https://github.com/clojure-emacs/sayid/blob/master/CHANGELOG.md#050---2026-07-01">Sayid 0.5.0</a> made recording bounded: a record limit, per-function limits, sampling, a max trace depth and bounded printing. Tracing a namespace under a test suite can’t eat all your memory anymore.</li>
  <li><a href="https://github.com/clojure-emacs/sayid/blob/master/CHANGELOG.md#060---2026-07-08">Sayid 0.6.0</a> rebuilt inner tracing on <code class="language-plaintext highlighter-rouge">tools.analyzer.jvm</code>, replacing the fragile source-rewriting instrumenter.</li>
  <li><a href="https://github.com/clojure-emacs/sayid/blob/master/CHANGELOG.md#070---2026-07-10">Sayid 0.7.0</a> added <code class="language-plaintext highlighter-rouge">sayid.data</code> (the recorded call tree as plain data, with <code class="language-plaintext highlighter-rouge">tap&gt;</code> integration for Portal and friends) and <code class="language-plaintext highlighter-rouge">sayid.golden</code>, a golden-trace testing helper.</li>
  <li><a href="https://github.com/clojure-emacs/sayid/blob/master/CHANGELOG.md#080---2026-07-17">Sayid 0.8.0</a> focused on the experience: a <code class="language-plaintext highlighter-rouge">sayid-menu</code> transient, plain-language feedback from the trace commands, getting-started hints in empty views, and a fix for the inspector integration that had been broken for years.</li>
</ul>

<p>I wrote a bit more about the last one <a href="/posts/2026/07/18/sayid-0-8.html">here</a>. Not bad for a project that was completely dead in June, right?</p>

<h2 id="drawbridge">Drawbridge</h2>

<p>Drawbridge is nREPL’s HTTP transport, created by Chas Emerick in 2012 and “technically maintained” ever since. I finally gave it the attention it needed:</p>

<ul>
  <li><a href="https://github.com/nrepl/drawbridge/releases/tag/v0.3.1">Drawbridge 0.3.1</a> updated the dependencies (nREPL 1.7, Ring 1.15) and throttled client polling so it stops flooding servers with GET requests.</li>
  <li><a href="https://github.com/nrepl/drawbridge/releases/tag/v0.4.0">Drawbridge 0.4.0</a> is the interesting one. It adds <code class="language-plaintext highlighter-rouge">drawbridge.bridge</code>, a local nREPL socket server that relays to a remote Drawbridge endpoint, so any socket-based client (CIDER, Calva, rebel-readline) can now talk to Drawbridge. There’s also a WebSocket transport with server push instead of long-polling, bearer-token authentication via <code class="language-plaintext highlighter-rouge">secure-ring-handler</code> (which refuses to run unauthenticated unless you insist), and a <code class="language-plaintext highlighter-rouge">deps.edn</code>, so it’s usable as a git dependency.</li>
</ul>

<p>The full story is in <a href="/posts/2026/07/14/lowering-the-drawbridge.html">Lowering the Drawbridge</a>.</p>

<h2 id="nrepl">nREPL</h2>

<p>No nREPL release this cycle, but master is shaping up nicely for 1.8:</p>

<ul>
  <li>the built-in command-line client can connect using a URL, including the <code class="language-plaintext highlighter-rouge">nrepls://</code> and <code class="language-plaintext highlighter-rouge">nrepl+unix:</code> URLs that TLS and filesystem-socket servers advertise, and <code class="language-plaintext highlighter-rouge">http(s)://</code> when Drawbridge is on the classpath</li>
  <li>TLS hardening: descriptive errors for invalid key material, Ed25519 keys, tolerating a swapped certificate order, and a <a href="https://nrepl.org/nrepl/usage/tls.html">documented security model</a></li>
  <li>the built-in client sends input to the server as raw text, so reader typos, auto-resolved keywords and custom tagged literals no longer crash it</li>
  <li>stdin fixes: EOF arriving behind buffered input is reported properly, and a race between the stdin consumer and producer is gone</li>
  <li><code class="language-plaintext highlighter-rouge">nrepl.spec</code> finally matches what <code class="language-plaintext highlighter-rouge">describe</code> and <code class="language-plaintext highlighter-rouge">ls-sessions</code> actually send</li>
  <li>a pile of documentation debt cleared (<code class="language-plaintext highlighter-rouge">lookup</code> return values, the <code class="language-plaintext highlighter-rouge">session-closed</code> status, the <code class="language-plaintext highlighter-rouge">-f</code>/<code class="language-plaintext highlighter-rouge">--repl-fn</code> option, <a href="https://nrepl.org/nrepl/building_middleware.html">middleware best practices</a>) and a CI check keeping <code class="language-plaintext highlighter-rouge">ops.adoc</code> in sync with the descriptors</li>
  <li>Clojure 1.10 is the new minimum and <code class="language-plaintext highlighter-rouge">nrepl.misc/requiring-resolve</code> is gone in favour of the core one</li>
</ul>

<p>The nrepl.org site also picked up links to several new clients and servers (Nautilos, nREPL.hx for Helix, Janet and Steel Scheme servers).
The nREPL family keeps growing, which makes me happy every single time.</p>

<h2 id="nrepl-on-the-beam">nREPL on the BEAM</h2>

<p><a href="https://github.com/nrepl/nrepl-beam">nrepl-beam</a> is a brand new project I started in July, mostly because I wanted to see how well the nREPL spec holds up
when implemented from scratch outside the JVM. It’s home to:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">dialtone</code>, an nREPL server for Erlang (and a server core for the whole BEAM)</li>
  <li><code class="language-plaintext highlighter-rouge">repartee</code>, the Elixir server built on top of it</li>
  <li><code class="language-plaintext highlighter-rouge">chaser</code>, a terminal nREPL client that works with any nREPL server</li>
</ul>

<p><a href="https://github.com/nrepl/nrepl-beam/releases/tag/v0.1.0">nrepl-beam 0.1.0</a> shipped on July 14. Both servers implement the full op set (eval with streamed output, sessions, interrupts, stdin, load-file, completions, lookup)
and pass <a href="https://github.com/nrepl/neat">neat</a>’s cross-implementation integration suite alongside Clojure, Babashka and Basilisp.
Writing them was a good test of the spec, and it produced a few of the documentation fixes listed above. Turns out that the best way to find holes in a spec is to implement it in a language you barely know.</p>

<h2 id="mezcaml">mezcaml</h2>

<p>In the same spirit, <a href="https://github.com/nrepl/mezcaml">mezcaml</a> is a minimal nREPL client for OCaml: a small client library plus a command-line REPL,
working against any nREPL server regardless of the language on the other end. No release yet, but the core protocol works, it reads whole forms,
and it has server-driven completion. Nothing serious - it was a fun way to combine my recent OCaml hacking with nREPL.</p>

<h2 id="clojure-mode-clojure-ts-mode-and-mranderson">clojure-mode, clojure-ts-mode and MrAnderson</h2>

<p>Smaller things: the <code class="language-plaintext highlighter-rouge">#_</code> toggle commands in clojure-mode were renamed to <code class="language-plaintext highlighter-rouge">clojure-toggle-discard</code> and friends (matching Clojure’s own terminology, old names kept as aliases),
both modes got a <code class="language-plaintext highlighter-rouge">:to-have-face</code> matcher for font-lock tests, and clojure-ts-mode now checks the indentation of its sources on CI.</p>

<p><a href="https://github.com/benedekfazekas/mranderson/releases/tag/v0.7.1">MrAnderson 0.7.1</a> added a command-line interface, so it can be run without Leiningen,
and reworked its downstream integration tests against cider-nrepl and refactor-nrepl, which had silently stopped exercising local changes. Oops.</p>

<h2 id="blog-posts">Blog posts</h2>

<p>I wrote a lot this summer, mostly a series on the notable changes in CIDER 2.0:</p>

<ul>
  <li><a href="/posts/2026/07/14/lowering-the-drawbridge.html">Lowering the Drawbridge</a></li>
  <li><a href="/posts/2026/07/15/cider-2-0.html">CIDER 2.0: Sky is the Limit</a></li>
  <li><a href="/posts/2026/07/16/clj-refactor-4-0.html">clj-refactor.el 4.0</a></li>
  <li><a href="/posts/2026/07/16/simplifying-session-management-in-cider.html">Simplifying Session Management in CIDER</a></li>
  <li><a href="/posts/2026/07/17/stepping-through-macros-in-cider.html">Stepping Through Macros in CIDER</a></li>
  <li><a href="/posts/2026/07/18/sayid-0-8.html">Sayid 0.8</a></li>
  <li><a href="/posts/2026/07/20/clj-suitable-0-8-0.html">clj-suitable 0.8.0: Closing the Gap with Compliment</a></li>
  <li><a href="/posts/2026/07/23/making-cider-more-discoverable.html">Making CIDER More Discoverable</a></li>
  <li><a href="/posts/2026/07/25/modernizing-cider-completion.html">Modernizing CIDER’s Completion</a></li>
  <li><a href="/posts/2026/07/27/closing-the-find-usages-gap-in-cider.html">Closing the Find-Usages Gap in CIDER</a></li>
  <li><a href="/posts/2026/07/28/sharpening-ciders-debugging-tools.html">Sharpening CIDER’s Debugging Tools</a></li>
  <li><a href="/posts/2026/07/29/leveling-up-ciders-clojurescript-support.html">Leveling Up CIDER’s ClojureScript Support</a></li>
  <li><a href="/posts/2026/08/26/smarter-form-targeting-is-coming-to-cider.html">Smarter Form Targeting Is Coming to CIDER</a></li>
  <li><a href="/posts/2026/08/29/smarter-form-targeting-is-not-coming-to-cider.html">Smarter Form Targeting Is Not Coming to CIDER</a></li>
</ul>

<h2 id="epilogue">Epilogue</h2>

<p>Big thanks to Clojurists Together, Nubank and the other organizations and people supporting my Clojure OSS work! None of this would have happened without you. You rock!</p>

<p>As for what’s next - CIDER 2.1 is the obvious milestone, and it’s mostly a matter of letting the clojure-ts-mode integration settle.
After that I’d like to cut nREPL 1.8 with the TLS and URL work, and get mezcaml and the BEAM servers to a point where they are useful
to someone other than me. I won’t make any predictions about productivity this time around. Clearly I’m bad at those.</p>

<p>Keep hacking!</p>]]></content><author><name>Bozhidar Batsov</name></author><category term="posts" /><category term="Clojure" /><category term="nREPL" /><category term="CIDER" /><category term="OSS" /><summary type="html"><![CDATA[Time for another bi-monthly update on the work that Clojurists Together are funding this year - my maintenance of nREPL, CIDER and friends. I published the previous one as a blog post for the first time and the feedback was good enough that I’ll keep doing it. Last time I said that I had plucked most of the low-hanging fruit and that the next two months were unlikely to be as productive. Well, I was wrong. CIDER 2.0 finally shipped, and once it was out the door I used the momentum to sweep through pretty much every corner of the nREPL/CIDER ecosystem. A few long-neglected projects got proper releases, and nREPL got a couple of brand new implementations in languages I play on the side from time to time.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://metaredux.com/assets/og-image.png" /><media:content medium="image" url="https://metaredux.com/assets/og-image.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Smarter Form Targeting Is Not Coming to CIDER</title><link href="https://metaredux.com/posts/2026/08/29/smarter-form-targeting-is-not-coming-to-cider.html" rel="alternate" type="text/html" title="Smarter Form Targeting Is Not Coming to CIDER" /><published>2026-08-29T06:37:00+00:00</published><updated>2026-08-29T06:37:00+00:00</updated><id>https://metaredux.com/posts/2026/08/29/smarter-form-targeting-is-not-coming-to-cider</id><content type="html" xml:base="https://metaredux.com/posts/2026/08/29/smarter-form-targeting-is-not-coming-to-cider.html"><![CDATA[<p>A couple of days ago I wrote that <a href="/posts/2026/08/26/smarter-form-targeting-is-coming-to-cider.html">smarter form targeting was coming to CIDER</a>,
and I ended that post by asking whether I’d got the resolution rules right and
whether anything still surprised people. I got an answer. CIDER 2.1 will ship
with the classic behaviour intact.</p>

<!--more-->

<p>This is not a sad story, though. The detour turned up a bug that was quietly
mangling people’s comments, and CIDER came out of it better than it went in.</p>

<h2 id="what-i-was-actually-after">What I was actually after</h2>

<p>The targeting change wasn’t really about cursor positions. What I wanted was
for <em>every</em> CIDER command that operates on a form to behave the same way,
without adding yet another command to get there. CIDER has an enormous surface
of evaluation commands, and every “at point” variant I could have added would
have made that worse. If the existing commands simply resolved the form you
meant, newcomers would have had fewer commands to learn, not more.</p>

<p>That was the bet: consistency by redefinition rather than by addition. In
hindsight it was the wrong bet for a project this old. Fifteen years in, the
existing behaviour isn’t an implementation detail I get to tidy up - it’s
the contract. And as it turned out, the redefinition wasn’t nearly as
transparent as I’d convinced myself it was.</p>

<h2 id="the-feedback">The feedback</h2>

<p>Several users, including CIDER’s co-maintainer Sashko Yakushev, voiced
concerns and flagged issues I’d overlooked while playing with this initially.
The most important one: inspection is just another flavour of evaluation, and
people inspect bare symbols constantly. If it got the same treatment, the
disruption would be real enough to run a fork over.</p>

<p>My instinct was that this was an edge case, so I measured it instead of
arguing. Across every cursor position in a buffer, only three rules actually
differ, and the flagship flow - type a form, hit <code class="language-plaintext highlighter-rouge">C-x C-e</code> - is identical
under both. Two of those three were fine. The third was this one:</p>

<p><img src="/assets/images/cider-closing-paren.gif" alt="Cursor on a closing paren: the classic rules evaluate the last form inside, smart targeting evaluates the whole enclosing call" /></p>

<p>The cursor doesn’t move between those two evaluations. That’s the same
position, twice, and the answers differ - <code class="language-plaintext highlighter-rouge">"b"</code> under the classic rules,
<code class="language-plaintext highlighter-rouge">"ab"</code> under the new ones.</p>

<p>I’d filed “cursor on a closing paren” as an oddity nobody hits deliberately.
But think about when you land there: you finish typing the last thing inside
a form, and the cursor is now sitting on the <code class="language-plaintext highlighter-rouge">)</code>. For someone inspecting
symbols all day, that fires constantly. And the classic answer isn’t
arbitrary either - as Sashko put it, the rule of thumb is “whatever
<code class="language-plaintext highlighter-rouge">paredit-backward</code> jumps back to”, which is a better description of the
tradition than anything I’d written down.</p>

<h2 id="why-the-tradition-exists-in-the-first-place">Why the tradition exists in the first place</h2>

<p>Here’s the part I under-weighted, and it’s worth spelling out for anyone who
finds “the form <em>before</em> the cursor” arbitrary.</p>

<p>Emacs form navigation overwhelmingly leaves the cursor <em>after</em> a form.
<code class="language-plaintext highlighter-rouge">C-M-f</code> (<code class="language-plaintext highlighter-rouge">forward-sexp</code>) moves over the next form and stops just past its
closing delimiter. <code class="language-plaintext highlighter-rouge">C-M-e</code> (<code class="language-plaintext highlighter-rouge">end-of-defun</code>) leaves you after the whole
top-level form. <code class="language-plaintext highlighter-rouge">C-M-n</code> (<code class="language-plaintext highlighter-rouge">forward-list</code>) does the same for the enclosing
list. Paredit’s <code class="language-plaintext highlighter-rouge">paredit-forward</code> behaves the same way, and so does typing:
finish a form and the cursor is, by definition, right after it.</p>

<p>So “evaluate the preceding form” isn’t a quirk - it <em>composes</em> with how you
already move around. Navigate forward over a form, evaluate it. Type a form,
evaluate it. The cursor is already in the right place, every time.</p>

<p>Getting <em>onto</em> a form instead takes deliberate effort: <code class="language-plaintext highlighter-rouge">C-M-b</code>
(<code class="language-plaintext highlighter-rouge">backward-sexp</code>), <code class="language-plaintext highlighter-rouge">C-M-a</code> (<code class="language-plaintext highlighter-rouge">beginning-of-defun</code>), <code class="language-plaintext highlighter-rouge">paredit-backward</code>, or a
jump package like <code class="language-plaintext highlighter-rouge">avy</code>. All perfectly good tools, but you have to reach for
them - unless you’re clicking around with a mouse, in which case the cursor
lands wherever you pointed and “the form before the cursor” genuinely is
useless. Which, I suspect, is exactly the workflow difference behind this
whole argument.</p>

<h2 id="what-cider-got-instead">What CIDER got instead</h2>

<p>The half of the idea that was never controversial is still there, as commands
you opt into rather than a new meaning for keys you already use. Every
operation now has an “at point” variant, not just eval and tap:
<code class="language-plaintext highlighter-rouge">cider-inspect-sexp-at-point</code>, <code class="language-plaintext highlighter-rouge">cider-pprint-eval-sexp-at-point</code>,
<code class="language-plaintext highlighter-rouge">cider-macroexpand-1-at-point</code>, <code class="language-plaintext highlighter-rouge">cider-macroexpand-all-at-point</code>,
<code class="language-plaintext highlighter-rouge">cider-format-edn-sexp-at-point</code>, <code class="language-plaintext highlighter-rouge">cider-insert-sexp-at-point-in-repl</code>.</p>

<p>Three ways to say which form you mean, and now every command supports all of
them:</p>

<p><img src="/assets/images/cider-form-selection.gif" alt="The same expression evaluated three ways: the preceding form, the form at the cursor, and the enclosing top-level form" /></p>

<p>The at-point commands fall back to the preceding form when there’s nothing to
point at, so they’re drop-in replacements rather than a separate mode of
working. Which means anyone who wanted smart targeting can simply have it:</p>

<div class="language-emacs-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nv">with-eval-after-load</span> <span class="ss">'cider-mode</span>
  <span class="p">(</span><span class="nv">define-key</span> <span class="nv">cider-mode-map</span> <span class="p">(</span><span class="nv">kbd</span> <span class="s">"C-x C-e"</span><span class="p">)</span> <span class="nf">#'</span><span class="nv">cider-eval-sexp-at-point</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">define-key</span> <span class="nv">cider-mode-map</span> <span class="p">(</span><span class="nv">kbd</span> <span class="s">"C-c C-e"</span><span class="p">)</span> <span class="nf">#'</span><span class="nv">cider-eval-sexp-at-point</span><span class="p">))</span>
</code></pre></div></div>

<p>Two lines, no hidden mode, and everyone else’s fingers keep working. This is
what I should have shipped in the first place, and it’s what the
<a href="https://docs.cider.mx/cider/usage/code_evaluation.html">manual</a> now
recommends. Yes, it’s more commands than I wanted. It’s also the version that
doesn’t break anyone.</p>

<p>Macroexpansion is the one place a bit of cleverness survived on its own
merits, because an expansion needs a call form. Stand on a bare symbol and
<code class="language-plaintext highlighter-rouge">cider-macroexpand-1-at-point</code> widens to the call around it, since expanding a
lone symbol is never what anyone meant.</p>

<h2 id="the-bug-at-the-bottom-of-the-hole">The bug at the bottom of the hole</h2>

<p>While unifying the plumbing I found this, which is much worse than anything
form targeting was ever guilty of. Put the cursor at the end of a comment:</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">defn</span><span class="w"> </span><span class="n">foo</span><span class="w"> </span><span class="p">[])</span><span class="w">
</span><span class="c1">;; a comment|</span><span class="w">
</span></code></pre></div></div>

<p>CIDER answered <code class="language-plaintext highlighter-rouge">comment</code>. Not the <code class="language-plaintext highlighter-rouge">(comment ...)</code> form - the <em>word</em>, lifted
out of your prose, because sexp motion has no notion of comments once the
cursor is inside one and happily reads the words as symbols.</p>

<p>For evaluation that produced a puzzling error. For the in-place macroexpansion
commands, which <em>replace</em> the region they resolved, it did this:</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">;; a comment          -&gt;   ;; a EXPANDED&lt;comment&gt;</span><span class="w">
</span><span class="p">(</span><span class="nb">+</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="mi">2</span><span class="p">)</span><span class="w"> </span><span class="c1">; hey         -&gt;   (+ 1 2)          ; EXPANDED&lt;hey&gt;</span><span class="w">
</span></code></pre></div></div>

<p>It rewrote the comment. That bug has been in CIDER for years, and I only found
it because I went looking at the primitives while cleaning up after myself.</p>

<p>Then I checked the neighbours, and this is my favourite part of the whole
episode: <strong>SLIME, SLY and Emacs Lisp itself all still do this.</strong> Both Lisp
environments use a bare <code class="language-plaintext highlighter-rouge">backward-sexp</code>, and if you put the cursor after
<code class="language-plaintext highlighter-rouge">(+ 1 2) ; hey</code> in any Emacs Lisp buffer and ask for the preceding sexp, you
get <code class="language-plaintext highlighter-rouge">hey</code>. CIDER now steps out of the comment first, which as far as I can
tell makes it the only one of the family that gets this right.<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup></p>

<h2 id="one-idea-worth-stealing">One idea worth stealing</h2>

<p>The same survey turned up something CIDER was missing. SLY briefly flashes the
region it compiled, so you <em>see</em> what it acted on. That’s a direct answer to
the confusion this whole saga was about - “which form did it just take?” -
and it changes nothing about what gets taken.</p>

<div class="language-emacs-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">setq</span> <span class="nv">cider-flash-evaluated-region</span> <span class="no">t</span><span class="p">)</span>
</code></pre></div></div>

<p>Off by default, because I’ve learned my lesson about switching things on for
everyone. But if the targeting rules ever puzzle you, turn it on for a day.</p>

<h2 id="the-moral">The moral</h2>

<p>In the original post I described <code class="language-plaintext highlighter-rouge">cider-form-targeting</code>, the escape hatch back
to the classic rules, as “living on borrowed time” - clutter left over from a
plan I’d since reversed, which I was itching to delete.</p>

<p>That option is the only reason the conversation stayed a conversation. Its
existence made the objection “please don’t remove the fallback” rather than
“I’m forking CIDER”, and I very nearly removed it before anyone had tried the
change.</p>

<p>Every mistake is a learning experience for me, and this one was cheap: nothing
had shipped, so the whole thing cost a few days and some rewriting. The
testing and feedback cycle worked exactly as it should have - people tried
something on <code class="language-plaintext highlighter-rouge">master</code>, told me plainly what was wrong with it, and the result
is better than either what I proposed or what we had before. Everyone gets
the behaviour they want, and CIDER lost a text-eating bug on the way.</p>

<p>Thanks to everyone who took the time to tell me I was wrong.</p>

<p>That’s all I have for you today. Keep hacking!</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>If you’re an Emacs maintainer reading this: <code class="language-plaintext highlighter-rouge">elisp--preceding-sexp</code> has the same behaviour, and I’d be happy to be told why it’s intentional. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Bozhidar Batsov</name></author><category term="posts" /><category term="Emacs" /><category term="Clojure" /><category term="CIDER" /><summary type="html"><![CDATA[A couple of days ago I wrote that smarter form targeting was coming to CIDER, and I ended that post by asking whether I’d got the resolution rules right and whether anything still surprised people. I got an answer. CIDER 2.1 will ship with the classic behaviour intact.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://metaredux.com/assets/og-image.png" /><media:content medium="image" url="https://metaredux.com/assets/og-image.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Smarter Form Targeting Is Coming to CIDER</title><link href="https://metaredux.com/posts/2026/08/26/smarter-form-targeting-is-coming-to-cider.html" rel="alternate" type="text/html" title="Smarter Form Targeting Is Coming to CIDER" /><published>2026-08-26T09:18:00+00:00</published><updated>2026-08-26T09:18:00+00:00</updated><id>https://metaredux.com/posts/2026/08/26/smarter-form-targeting-is-coming-to-cider</id><content type="html" xml:base="https://metaredux.com/posts/2026/08/26/smarter-form-targeting-is-coming-to-cider.html"><![CDATA[<p>If I had a dollar for every time someone asked on the Clojurians Slack in
<code class="language-plaintext highlighter-rouge">#cider</code> why <code class="language-plaintext highlighter-rouge">C-x C-e</code> evaluated “the wrong thing”, I’d probably be writing this
post from a yacht.  The answer was always the same: the cursor wasn’t where
CIDER expected it to be. The <em>upcoming</em> CIDER 2.1 release changes that - the
evaluation commands now figure out which form you mean from where your cursor
actually is.</p>

<!--more-->

<p>NOTE: <strong>Update:</strong> this didn’t survive contact with its users, and CIDER 2.1
ships with the classic behaviour after all. See
/posts/2026/08/29/smarter-form-targeting-is-not-coming-to-cider.html for
what the feedback was, what replaced it, and the rather nasty bug the detour
turned up.</p>

<h2 id="a-bit-of-history">A bit of history</h2>

<p>Emacs has a very particular tradition when it comes to evaluating code:
<code class="language-plaintext highlighter-rouge">eval-last-sexp</code> (the venerable <code class="language-plaintext highlighter-rouge">C-x C-e</code>) acts on the expression <em>before</em> the
cursor. Not the one you’re looking at, not the one you’re inside of - the one
that ends exactly where your cursor stands. SLIME follows this tradition,
Emacs Lisp itself follows it, and for the past 15+ years CIDER has followed it
too.<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> If you grew up in Emacs, this rule is in your fingers and you’ve
never once thought about it.</p>

<p>Here’s the thing, though - most people using CIDER didn’t grow up in Emacs.
And many of them never programmed in Emacs Lisp and Common Lisp with SLIME.
They came to Emacs <em>because of</em> CIDER (or Clojure in general), and for them
the rule is invisible, arbitrary and mildly hostile. You put your cursor on a
form, you press the eval key, and CIDER cheerfully evaluates… something
else. Meanwhile every other modern Clojure environment - Calva, Conjure, the
various vim plugins - resolves the form from the cursor position and just does
what you meant.<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup></p>

<p>For a long time I resisted changing this, mostly out of respect for the Emacs
tradition (and my own muscle memory). But at some point I had to admit that I
was optimizing for the wrong audience. CIDER’s users are mostly casual
Clojure hackers who happen to use Emacs, not Emacs experts who happen to
write Clojure. The tradition was serving me, and confusing them.</p>

<h2 id="whats-actually-changing">What’s actually changing</h2>

<p>The evaluation commands (and their macroexpansion, inspection and tapping
siblings) now resolve “the form the cursor indicates”. Concretely, with <code class="language-plaintext highlighter-rouge">|</code>
marking the cursor:</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nb">map</span><span class="w"> </span><span class="nb">inc</span><span class="w"> </span><span class="err">|</span><span class="p">(</span><span class="nb">range</span><span class="w"> </span><span class="mi">10</span><span class="p">))</span><span class="w">
</span></code></pre></div></div>

<p>Pressing <code class="language-plaintext highlighter-rouge">C-c C-e</code> here used to evaluate <code class="language-plaintext highlighter-rouge">inc</code> - the form <em>before</em> the cursor,
which is almost never what you wanted. Now it evaluates <code class="language-plaintext highlighter-rouge">(range 10)</code> - the
form your cursor is pointing at.</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nb">str</span><span class="w"> </span><span class="s">"hello"</span><span class="w"> </span><span class="s">" "</span><span class="w"> </span><span class="s">"world"</span><span class="err">|</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<p>This one used to evaluate <code class="language-plaintext highlighter-rouge">"world"</code> (really!), because the last complete
expression before a cursor sitting on the closing paren is the final string.
Now it evaluates the whole <code class="language-plaintext highlighter-rouge">(str ...)</code> call.</p>

<p>Macroexpansion benefits too:</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nb">when</span><span class="w"> </span><span class="n">tru</span><span class="err">|</span><span class="n">e</span><span class="w"> </span><span class="p">(</span><span class="nf">launch-missiles</span><span class="p">))</span><span class="w">
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">C-c C-m</code> here used to complain that <code class="language-plaintext highlighter-rouge">true</code> is not a macro. Now it expands the
enclosing <code class="language-plaintext highlighter-rouge">(when ...)</code> call, because expanding a bare symbol is never what
anyone means.</p>

<p>And my favorite one - the rich comment workflow is now consistent everywhere:</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nb">comment</span><span class="w">
  </span><span class="p">(</span><span class="nf">calculate-all-the-things</span><span class="err">|</span><span class="p">))</span><span class="w">
</span></code></pre></div></div>

<p>Every defun-level command - eval, pretty-print, inspect, debug - now treats
the form inside the <code class="language-plaintext highlighter-rouge">(comment ...)</code> as the top-level one. Evaluating a whole
<code class="language-plaintext highlighter-rouge">comment</code> form returns <code class="language-plaintext highlighter-rouge">nil</code> by definition, which has exactly zero uses, so
CIDER no longer does that no matter which command you reach for.</p>

<h2 id="why-you-probably-wont-notice">Why you probably won’t notice</h2>

<p>Here’s the part I’m most pleased with: the new behavior agrees with the old
one at every position where “the form before the cursor” made sense. Cursor
right after a form? Same result as always. Cursor in the whitespace after a
form? Same. Cursor in the middle of a symbol? Same. The two behaviors only
diverge where the classic answer was something nobody ever wanted - a
previous sibling, a lone trailing atom.</p>

<p>So if your muscle memory follows the Emacs tradition, nothing changes for
you. If it doesn’t - CIDER stops punishing you for it. That’s the whole
change.</p>

<h2 id="reverting-to-the-classic-behavior-for-now">Reverting to the classic behavior (for now)</h2>

<p>If you <em>do</em> want the traditional rules - maybe you genuinely use
“evaluate the previous sibling while standing on an opening paren” - one
setting restores them exactly:</p>

<div class="language-emacs-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">setq</span> <span class="nv">cider-form-targeting</span> <span class="ss">'preceding</span><span class="p">)</span>
</code></pre></div></div>

<p>There’s also a per-session toggle in the eval menu (<code class="language-plaintext highlighter-rouge">C-c C-v T</code>) that shows
the active mode in the mode line while you experiment.</p>

<p>This option is probably living on borrowed
time, though. I added it back when I planned to keep the classic behavior as the
default and offer smart targeting as an opt-in. Now that the roles are
reversed, an option whose only job is restoring rules almost nobody
deliberately relied on doesn’t really make much sense, and lately I’ve been
trying to trim that kind of clutter from CIDER, not add to it. Don’t be
surprised if the option quietly disappears - possibly even before the release
ships. Which is one more reason to speak up now if the classic behavior
genuinely matters to you.</p>

<h2 id="farewell-last-sexp">Farewell, “last sexp”</h2>

<p>This change forced my hand on something I’d been putting off for years - the
command names. <code class="language-plaintext highlighter-rouge">cider-eval-last-sexp</code> is a fine name for a command that
evaluates the last sexp. It’s a lie for a command that evaluates the form
your cursor indicates. So the commands got honest names:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">cider-eval-last-sexp</code> is now <code class="language-plaintext highlighter-rouge">cider-eval-form</code></li>
  <li><code class="language-plaintext highlighter-rouge">cider-eval-defun-at-point</code> is now <code class="language-plaintext highlighter-rouge">cider-eval-defun</code> (the <code class="language-plaintext highlighter-rouge">-at-point</code>
never carried information)</li>
  <li>likewise for the pprint/tap/inspect/insert variants</li>
</ul>

<p>Every old name keeps working as an alias, so your config and your <code class="language-plaintext highlighter-rouge">M-x</code>
habits are safe. But why “form” and not “sexp”? Beyond the targeting change,
there’s a Clojure-specific reason: in Clojure a <em>form</em> isn’t always a single
sexp. <code class="language-plaintext highlighter-rouge">^:private x</code> is two sexps but one form; so is <code class="language-plaintext highlighter-rouge">#inst "2024-01-01"</code>.
The commands operate on forms - the reader’s unit of evaluation - and now
they say so.<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup> The manual’s evaluation docs got a proper glossary
explaining all of this.</p>

<h2 id="closing-thoughts">Closing thoughts</h2>

<p>All of this is on <code class="language-plaintext highlighter-rouge">master</code> and in the MELPA snapshots today, ahead of the
next stable release. I’d really love for people to play with it <em>before</em> the
release ships - especially if you’re an Emacs veteran whose fingers disagree
with my reasoning, or a newcomer for whom this was supposed to just work. Did
we get the resolution rules right? Does anything still surprise you?</p>

<p>Share your feedback on the <a href="https://github.com/clojure-emacs/cider/discussions">CIDER discussions</a> board, in
<code class="language-plaintext highlighter-rouge">#cider</code> on the Clojurians Slack, or just file an issue. This is exactly the
kind of change that’s easy to adjust before a release and painful after -
and the fate of the compatibility option depends on what I hear.</p>

<p>That’s all I have for you today. Keep hacking!</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>CIDER started its life as a SLIME “clone” for Clojure, after all - the tradition runs deep. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p>Interestingly, Cursive is the only major non-Emacs Clojure environment that kept the classic “form before the caret” model. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3" role="doc-endnote">
      <p>This also explains a subtlety Emacs veterans might appreciate: plain <code class="language-plaintext highlighter-rouge">forward-sexp</code> movement doesn’t know that Clojure metadata belongs to the form it annotates, which is why clojure-mode has always needed its own “logical sexp” movement functions. The new targeting is built on those, so metadata is never silently dropped from what you evaluate. <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Bozhidar Batsov</name></author><category term="posts" /><category term="Emacs" /><category term="Clojure" /><category term="CIDER" /><summary type="html"><![CDATA[If I had a dollar for every time someone asked on the Clojurians Slack in #cider why C-x C-e evaluated “the wrong thing”, I’d probably be writing this post from a yacht. The answer was always the same: the cursor wasn’t where CIDER expected it to be. The upcoming CIDER 2.1 release changes that - the evaluation commands now figure out which form you mean from where your cursor actually is.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://metaredux.com/assets/og-image.png" /><media:content medium="image" url="https://metaredux.com/assets/og-image.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Projectile 3.4</title><link href="https://metaredux.com/posts/2026/08/13/projectile-3-4.html" rel="alternate" type="text/html" title="Projectile 3.4" /><published>2026-08-13T07:30:00+00:00</published><updated>2026-08-13T07:30:00+00:00</updated><id>https://metaredux.com/posts/2026/08/13/projectile-3-4</id><content type="html" xml:base="https://metaredux.com/posts/2026/08/13/projectile-3-4.html"><![CDATA[<p><a href="https://github.com/bbatsov/projectile/releases/tag/v3.4.0">Projectile 3.4</a> is out! After
the four releases that preceded it this summer, this one is polish rather than ambition.</p>

<p>Most of it came out of an annoyance I’d been living with for years without ever quite
naming it: a lot of what I work on isn’t one directory. <a href="https://github.com/clojure-emacs/cider">CIDER</a> is really CIDER plus
<a href="https://github.com/clojure-emacs/cider-nrepl">cider-nrepl</a> plus <a href="https://github.com/clojure-emacs/orchard">orchard</a> plus <a href="https://github.com/clojure-emacs/clj-refactor.el">clj-refactor</a> plus a
handful of others. <a href="https://github.com/rubocop/rubocop">RuboCop</a> is RuboCop plus <a href="https://github.com/rubocop/rubocop-ast">rubocop-ast</a> plus
<a href="https://github.com/rubocop/rubocop-rails">rubocop-rails</a> and the other extension gems plus <a href="https://github.com/rubocop/ruby-style-guide">the style
guide</a>. I’ve been maintaining families of repositories for the better
part of a decade.</p>

<p>Lately I’ve also been reaching for git worktrees far more than I used to, so the same
repository is now checked out two or three times on my disk at any given moment.</p>

<p>Projectile saw all of that as a pile of unrelated projects, which is exactly what it looks
like from the outside.</p>

<!--more-->

<h2 id="somewhere-else-in-the-same-project">Somewhere else in the same project</h2>

<p><code class="language-plaintext highlighter-rouge">projectile-switch-worktree</code> (<code class="language-plaintext highlighter-rouge">s-p W</code>) offers the other checkouts of the repository you’re
in, each labelled with whatever tells it apart - the branch for git, the workspace name for
Jujutsu:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Switch to worktree:
~/src/myapp-main/     (main)
~/src/myapp-hotfix/   (hotfix/crash-on-open)
</code></pre></div></div>

<p>Git worktrees are the obvious case. Projectile asks git about those, so a worktree you’ve
never opened in Emacs shows up anyway. Jujutsu workspaces work the same way.</p>

<p>The case I care about more is the one without any plumbing: a second <code class="language-plaintext highlighter-rouge">git clone</code> of the
same upstream. Same workflow, done by hand. I looked at my own <code class="language-plaintext highlighter-rouge">~/projects</code> while writing
this and found four separate clones of CIDER sitting in it. Nothing records those anywhere,
so Projectile can’t ask git to list them - it matches them among your known projects by
their remote instead.</p>

<h2 id="somewhere-else-in-the-same-effort">Somewhere else in the same <em>effort</em></h2>

<p>The other half is <code class="language-plaintext highlighter-rouge">projectile-switch-sibling-project</code> (<code class="language-plaintext highlighter-rouge">s-p n</code>), which offers the projects
related to the one you’re in rather than every project on the machine.</p>

<p>The interesting question was what “related” should mean. My first instinct was to compare
directory names and look for a shared prefix. Before writing anything I tried the
candidates out on my actual project directory, all ninety-odd repositories of it.</p>

<p>Comparing names does find <code class="language-plaintext highlighter-rouge">rubocop</code>, <code class="language-plaintext highlighter-rouge">rubocop-ast</code> and <code class="language-plaintext highlighter-rouge">rubocop-rails</code>. But grouping by the
<em>owner of the upstream remote</em> finds this:</p>

<p><img src="/assets/images/projectile-sibling-projects.png" alt="The sibling project prompt in CIDER, listing sixteen related projects including orchard, haystack, port and sayid" /></p>

<p>Half of those names have nothing in common with <code class="language-plaintext highlighter-rouge">cider</code>, and no amount of staring at
directory names would ever have related them. <a href="https://github.com/clojure-emacs/haystack">haystack</a> parses stack traces,
<a href="https://github.com/clojure-emacs/port">port</a> is a printer registry, <a href="https://github.com/clojure-emacs/sayid">sayid</a> is a tracing debugger. Nothing in those
names says “CIDER”.</p>

<p>What they do have in common is an org: they all live under
<a href="https://github.com/clojure-emacs">clojure-emacs</a> on GitHub. That’s not incidental, it’s how the project is
actually organised - the org is the boundary of the effort, and the individual repositories
are just where the pieces ended up when they got big enough to split out. Same story for
the <a href="https://github.com/rubocop">rubocop</a> org, and for <a href="https://github.com/nrepl">nrepl</a>. Once I saw that, keying on the
owner of the remote was obviously right, because it’s the same fact GitHub is already
recording for me.</p>

<p>It needs a bound, though, and the same experiment showed why: about 40% of my checkouts are
under my own GitHub account, and “we’re both under <code class="language-plaintext highlighter-rouge">bbatsov</code>” doesn’t relate anything. So an
inferred group covering more than a quarter of your known projects gets dropped and the
next signal takes over.<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> If you’d rather just say what belongs together,
<code class="language-plaintext highlighter-rouge">projectile-project-groups</code> is there and is never second-guessed.</p>

<p>Switching is only the obvious thing to do with this. Now that Projectile can tell which
projects belong together, the same grouping could back a find-file or a search across the
whole family - one prompt that reaches every repository in the org rather than just the one
you happen to be sitting in. I go looking for “which of these fifteen repos defines this
var” often enough that I suspect it’s the more useful half. Nothing built yet, but that’s
where I expect this to go.</p>

<h2 id="the-commands-come-with-you">The commands come with you</h2>

<p>Once Projectile knows two directories are the same repository, the command history can
follow you between them.</p>

<p>Press <code class="language-plaintext highlighter-rouge">M-p</code> at the compile or test prompt in a worktree you made this morning and you get
the commands the project is actually built with, instead of an empty history. That’s <a href="https://github.com/bbatsov/projectile/issues/1786">an
issue from 2022</a> that a stale bot had
helpfully closed for me at some point.</p>

<p>What doesn’t follow you is anything that runs without asking - what a prompt is pre-filled
with, and what <code class="language-plaintext highlighter-rouge">projectile-repeat-last-command</code> replays. I had those shared too in the
first draft, and then watched a repeat in one worktree rebuild the tree next door. A
remembered command can carry absolute paths back to where it was typed.</p>

<h2 id="more-languages-fewer-surprises">More languages, fewer surprises</h2>

<p><code class="language-plaintext highlighter-rouge">projectile-run-test-at-point</code> (<code class="language-plaintext highlighter-rouge">s-p c .</code>) arrived in 3.1 knowing Python, Go and JS/TS. It
now also knows Ruby (both RSpec and Minitest), Rust, Elixir, Java, Erlang and F#.</p>

<p>Ruby is written the same way whichever framework you use, so there the project type picks
the runner rather than the syntax; Java’s picks between Maven and Gradle. Elixir tests get
addressed as <code class="language-plaintext highlighter-rouge">FILE:LINE</code>, because ExUnit can’t select a test by name from the command line.
OCaml deliberately gets nothing - its tests are ordinary values you register with Alcotest
or OUnit, so there’s no syntax to recognize.</p>

<p><code class="language-plaintext highlighter-rouge">projectile-find-file-of-kind</code> (<code class="language-plaintext highlighter-rouge">s-p j</code>) and <code class="language-plaintext highlighter-rouge">projectile-toggle-related-file</code> (<code class="language-plaintext highlighter-rouge">s-p J</code>)
learned Phoenix, Laravel and Next.js. Rails and Django had been the only frameworks with
file-kinds tables out of the box, which felt like a strange place to have stopped.</p>

<h2 id="the-reports-answer-back">The reports answer back</h2>

<p>The dashboard and the doctor from 3.3 both got a pass. They’re no longer plain text -
section headers, field labels and findings are faced by meaning, with findings colored by
severity and sorted so anything wanting your attention comes first. The faces only inherit
from standard ones, so your theme styles them without knowing Projectile exists.</p>

<p>The doctor’s findings now come with a button that acts on them:</p>

<p><img src="/assets/images/projectile-doctor-findings.png" alt="The doctor's findings section, with a warning about projectile-mode carrying an enable button, above four ok findings" /></p>

<p><code class="language-plaintext highlighter-rouge">[enable]</code> for a <code class="language-plaintext highlighter-rouge">projectile-mode</code> you forgot to turn on, <code class="language-plaintext highlighter-rouge">[enable caching]</code> on a big
uncached project, <code class="language-plaintext highlighter-rouge">[open dirconfig]</code>, <code class="language-plaintext highlighter-rouge">[edit .dir-locals.el]</code>. Pressing one regenerates the
report. Findings Projectile can’t act on stay plain advice.</p>

<p>And since a doctor report usually ends up pasted into an issue, <code class="language-plaintext highlighter-rouge">w</code> copies the buffer as
plain text, without the faces and buttons.</p>

<h2 id="odds-and-ends">Odds and ends</h2>

<ul>
  <li><code class="language-plaintext highlighter-rouge">projectile-find-changed-file</code> (<code class="language-plaintext highlighter-rouge">s-p C</code>) completes over what git reports as staged,
unstaged or untracked - or, with a prefix argument, everything that differs from a
revision you pick.</li>
  <li><code class="language-plaintext highlighter-rouge">projectile-run-task</code> discovers rake tasks now, read out of your <code class="language-plaintext highlighter-rouge">Rakefile</code> and <code class="language-plaintext highlighter-rouge">.rake</code>
files rather than by running <code class="language-plaintext highlighter-rouge">rake -T</code>, which would load the whole application.</li>
  <li><code class="language-plaintext highlighter-rouge">projectile-ignored-project-patterns</code> is the regexp-matching sibling of
<code class="language-plaintext highlighter-rouge">projectile-ignored-projects</code>, so keeping a whole area of your machine out of the known
projects no longer needs a lambda.</li>
  <li>Messages Projectile emits on its own initiative are prefixed with <code class="language-plaintext highlighter-rouge">[Projectile]</code> now, and
the ones answering a command you just invoked aren’t. There were five different
conventions in there before, which I’d somehow never noticed.</li>
  <li>The known projects file, the frecency store and the session directory are resolved with
<code class="language-plaintext highlighter-rouge">locate-user-emacs-file</code>, so they land in the right place if your configuration lives in
<code class="language-plaintext highlighter-rouge">~/.config/emacs</code>. Nothing moves if it doesn’t.</li>
  <li>Elixir’s implementation/test toggle offers <code class="language-plaintext highlighter-rouge">foo_test.exs</code> rather than <code class="language-plaintext highlighter-rouge">foo_test.ex</code>, a
script ExUnit will actually run. Project types can declare their test file extension now.</li>
</ul>

<h2 id="upgrade-notes">Upgrade notes</h2>

<p>Nothing here should break a working setup, but two things are worth knowing.</p>

<p>A batch of options were renamed or folded together. The six
<code class="language-plaintext highlighter-rouge">projectile-&lt;cmd&gt;-use-comint-mode</code> options became one <code class="language-plaintext highlighter-rouge">projectile-use-comint-mode</code>;
<code class="language-plaintext highlighter-rouge">projectile-per-project-compilation-buffer</code> and <code class="language-plaintext highlighter-rouge">projectile-per-command-compilation-buffer</code>
became <code class="language-plaintext highlighter-rouge">projectile-compilation-buffer-scope</code>; and a handful of options that had broken
their own naming schemes were renamed to match their siblings.<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup> <strong>Every one of them is
still honored under its old name</strong>, so your config keeps working - you’ll just see an
obsolescence notice.</p>

<p>Two options are gone: <code class="language-plaintext highlighter-rouge">projectile-tags-file-name</code> and <code class="language-plaintext highlighter-rouge">projectile-go-project-test-function</code>
were only ever read as Projectile loaded, which means setting them from your init file
afterwards did precisely nothing.</p>

<p>And the command history is now the repository’s rather than the directory’s. Set
<code class="language-plaintext highlighter-rouge">projectile-command-history-scope</code> to <code class="language-plaintext highlighter-rouge">project</code> if you’d rather have it per directory.
Histories you already have are adopted, not dropped.</p>

<p>The full changelog is
<a href="https://github.com/bbatsov/projectile/blob/master/CHANGELOG.md">here</a>, and the manual is
at <a href="https://docs.projectile.mx/">docs.projectile.mx</a>. The cross-repository features have a
<a href="https://docs.projectile.mx/projectile/3.4/across_repositories.html">page of their own</a>,
limitations included.</p>

<p>Five releases in six weeks is not a pace I intend to keep up, and this is the natural place
for the burst to land. The big pieces from 3.0 through 3.3 have had their corners sanded
down, and what’s left on my list is smaller and less interesting to write about. Which is
roughly where a fifteen-year-old package ought to be.</p>

<p>That’s all I have for you today. Keep hacking!</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>Which is why <code class="language-plaintext highlighter-rouge">projectile</code> itself comes back with no siblings on my machine. The cap is working; the answer is a configured group. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p><code class="language-plaintext highlighter-rouge">projectile-global-ignore-file-patterns</code>, <code class="language-plaintext highlighter-rouge">projectile-cmd-hist-ignoredups</code>, <code class="language-plaintext highlighter-rouge">projectile-related-files-fn-function</code>, <code class="language-plaintext highlighter-rouge">projectile-auto-discover</code>, and the three reviewable-search options that were named after replace. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Bozhidar Batsov</name></author><category term="posts" /><category term="Emacs" /><category term="Projectile" /><summary type="html"><![CDATA[Projectile 3.4 is out! After the four releases that preceded it this summer, this one is polish rather than ambition. Most of it came out of an annoyance I’d been living with for years without ever quite naming it: a lot of what I work on isn’t one directory. CIDER is really CIDER plus cider-nrepl plus orchard plus clj-refactor plus a handful of others. RuboCop is RuboCop plus rubocop-ast plus rubocop-rails and the other extension gems plus the style guide. I’ve been maintaining families of repositories for the better part of a decade. Lately I’ve also been reaching for git worktrees far more than I used to, so the same repository is now checked out two or three times on my disk at any given moment. Projectile saw all of that as a pile of unrelated projects, which is exactly what it looks like from the outside.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://metaredux.com/assets/og-image.png" /><media:content medium="image" url="https://metaredux.com/assets/og-image.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">RuboCop 1.89: Project-Wide Analysis with Rubydex</title><link href="https://metaredux.com/posts/2026/08/05/rubocop-1-89.html" rel="alternate" type="text/html" title="RuboCop 1.89: Project-Wide Analysis with Rubydex" /><published>2026-08-05T07:00:00+00:00</published><updated>2026-08-05T07:00:00+00:00</updated><id>https://metaredux.com/posts/2026/08/05/rubocop-1-89</id><content type="html" xml:base="https://metaredux.com/posts/2026/08/05/rubocop-1-89.html"><![CDATA[<p><a href="https://github.com/rubocop/rubocop/releases/tag/v1.89.0">RuboCop 1.89</a> is out, and it comes with the usual assortment of new cops
and bug fixes. But there’s one theme running through this release that I’m
particularly excited about, and that’s what I want to talk about today: RuboCop
is finally learning to look beyond a single file.</p>

<!--more-->

<h2 id="the-single-file-curse-limitation">The single-file curse (limitation)</h2>

<p>For its entire existence RuboCop has analyzed your code one file at a time. That’s
not an accident - it’s a deliberate design choice that has served the project
extremely well. A file-at-a-time analyzer is simple, it’s fast, and it
parallelizes trivially across all your cores. It’s a big part of why RuboCop can
lint a huge codebase in a few seconds.</p>

<p>The catch is that a cop looking at a single file is essentially working with
blinders on. It has no idea what’s defined in the rest of your project. And that
turns out to be a real limitation for a whole class of checks:</p>

<ul>
  <li>Is this method a duplicate of one defined in another file? No way to tell.</li>
  <li>Does this constant actually refer to what I think it does, given the surrounding
namespaces? Can’t resolve it without the whole picture.</li>
  <li>Does this class really need a <code class="language-plaintext highlighter-rouge">super</code> call in its constructor, or does none of
its ancestors define <code class="language-plaintext highlighter-rouge">initialize</code>? RuboCop can’t see the ancestry, so it has to
play it safe.</li>
</ul>

<p>Faced with questions like these, cops have only ever had two options: bail out and
say nothing (a false negative), or guess and risk yelling at perfectly good code
(a false positive). Neither is great. Over the years we’ve papered over this with
configuration knobs - things like <code class="language-plaintext highlighter-rouge">AllowedParentClasses</code> for <code class="language-plaintext highlighter-rouge">Lint/MissingSuper</code>,
where you manually list the base classes RuboCop can’t see for itself. It works,
but it’s the kind of busywork a tool should be doing for you.</p>

<h2 id="enter-rubydex">Enter rubydex</h2>

<p><a href="https://github.com/Shopify/rubydex">rubydex</a> is a library from Shopify that does exactly what RuboCop
historically couldn’t: it builds a project-wide index of your code - a graph of
every declaration and reference, with the ancestry and resolution worked out. Give
a cop access to that index and suddenly it can answer all those cross-file
questions with confidence.</p>

<p>We introduced optional support for rubydex back in <a href="https://docs.rubocop.org/rubocop/usage/project_index.html">1.87</a>, and 1.89
is where it really starts to pay off.</p>

<p>I want to stress the word <em>optional</em> here. rubydex is a separate gem with native
(Rust) extensions, and using it means paying for an indexing pass and some extra
memory. That’s a perfectly reasonable trade for the extra smarts, but it’s not a
trade everyone wants to make, and it shouldn’t be forced on anyone. RuboCop’s
default experience should stay lean and dependency-light - no native code, no
index, no surprises. So rubydex is opt-in, and I fully expect it’ll stay that way
for the foreseeable future. If you want the cross-file superpowers, you flip a
switch; if you don’t, nothing changes for you.</p>

<p>Turning it on is two steps. Add the gem to your <code class="language-plaintext highlighter-rouge">Gemfile</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">gem</span> <span class="s1">'rubydex'</span><span class="p">,</span> <span class="ss">require: </span><span class="kp">false</span>
</code></pre></div></div>

<p>And enable the flag in your <code class="language-plaintext highlighter-rouge">.rubocop.yml</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">AllCops</span><span class="pi">:</span>
  <span class="na">UseProjectIndex</span><span class="pi">:</span> <span class="no">true</span>
</code></pre></div></div>

<p>Everything below happens automatically once the index is available - there’s no
per-cop opt-in. And if the gem isn’t installed, RuboCop just prints a friendly
warning and falls back to its usual file-local behavior.</p>

<h2 id="what-the-index-unlocks-in-189">What the index unlocks in 1.89</h2>

<p>This release wires the index into a good chunk of the cop library. Here’s a tour
of what you get.</p>

<p>There are a few brand-new cops that simply couldn’t exist without a project-wide
view:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Lint/NameTypo</code> is my personal favorite - it catches typos in constant and
method names by checking them against everything defined in your project and
suggesting the closest match. So <code class="language-plaintext highlighter-rouge">Services::UserCraetor.new</code> gets flagged with a
helpful “did you mean <code class="language-plaintext highlighter-rouge">UserCreator</code>?”, and the same goes for a fat-fingered
method call like <code class="language-plaintext highlighter-rouge">Report.generate_sumary</code>. It’s careful to only speak up when
it’s confident (the name has to be unresolvable <em>and</em> have a close sibling), so
it stays quiet on genuinely external names.</li>
  <li><code class="language-plaintext highlighter-rouge">Lint/DeprecatedReference</code> flags calls and constant references to any API you’ve
marked with a YARD <code class="language-plaintext highlighter-rouge">@deprecated</code> tag, anywhere in the project. Tag a method as
deprecated in one file, and every stray use of it elsewhere lights up. It’s a
lovely way to drive a gradual migration off some old code.</li>
  <li><code class="language-plaintext highlighter-rouge">Lint/UnusedPrivateMethod</code> does project-wide dead-code detection - it reports
private methods that are never referenced anywhere in the indexed project. It’s
disabled by default and best used as an occasional sweep, since it can’t see
reflective calls, but it’s great for a spring cleaning.</li>
</ul>

<p>A whole bunch of existing cops got sharper too, either catching things they used
to miss or backing off where they used to nag:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Lint/DuplicateMethods</code> now catches duplicates that live in <em>different</em> files -
the same method defined twice across two files was previously invisible to it.
If you’re deliberately redefining a method (a monkey patch, say), the self-alias
trick (<code class="language-plaintext highlighter-rouge">alias foo foo</code> right before the redefinition) signals your intent and
keeps things quiet.</li>
  <li><code class="language-plaintext highlighter-rouge">Lint/ConstantReassignment</code> likewise catches a constant that’s reassigned in
another file, not just within the current one.</li>
  <li><code class="language-plaintext highlighter-rouge">Lint/MissingSuper</code> no longer nags you about a missing <code class="language-plaintext highlighter-rouge">super</code> when the index can
prove that none of the class’s ancestors actually defines <code class="language-plaintext highlighter-rouge">initialize</code> - in that
case <code class="language-plaintext highlighter-rouge">super</code> would only reach the no-op <code class="language-plaintext highlighter-rouge">Object#initialize</code>. This is the one that
makes those <code class="language-plaintext highlighter-rouge">AllowedParentClasses</code> lists mostly unnecessary; RuboCop figures out
your abstract base classes on its own now.</li>
  <li><code class="language-plaintext highlighter-rouge">Lint/ConstantResolution</code> used to be so noisy it was practically unusable without
a pile of <code class="language-plaintext highlighter-rouge">Only</code>/<code class="language-plaintext highlighter-rouge">Ignore</code> configuration. With the index it reports only genuinely
ambiguous constants - ones that resolve differently through the surrounding
nesting than they would fully qualified - which finally makes it practical to
turn on.</li>
  <li><code class="language-plaintext highlighter-rouge">Lint/InheritException</code> now spots classes that inherit from <code class="language-plaintext highlighter-rouge">Exception</code>
indirectly, through a parent class defined elsewhere in your project.</li>
  <li><code class="language-plaintext highlighter-rouge">Style/Documentation</code> accepts a reopened class or module as documented as long as
<em>any</em> of its definition sites carries a doc comment, so you’re not forced to
repeat yourself at every reopening.</li>
  <li>A cluster of cops learned to stop producing false positives once they can see the
whole picture: <code class="language-plaintext highlighter-rouge">Style/MissingRespondToMissing</code> accepts a <code class="language-plaintext highlighter-rouge">respond_to_missing?</code>
defined in another reopening of the class; <code class="language-plaintext highlighter-rouge">Style/StaticClass</code> leaves alone
classes that are subclassed elsewhere; <code class="language-plaintext highlighter-rouge">Style/RedundantConstantBase</code> can now flag
a redundant leading <code class="language-plaintext highlighter-rouge">::</code> inside a namespace when it proves the constant resolves
identically without it; and <code class="language-plaintext highlighter-rouge">Naming/PredicatePrefix</code> and
<code class="language-plaintext highlighter-rouge">Naming/AccessorMethodName</code> stop suggesting renames for methods that override an
ancestor defined elsewhere in the project.</li>
  <li><code class="language-plaintext highlighter-rouge">Style/ClassAndModuleChildren</code> uses the index to make its (unsafe) autocorrection
a lot more reliable - it resolves the real namespace kind (<code class="language-plaintext highlighter-rouge">class</code> vs <code class="language-plaintext highlighter-rouge">module</code>)
instead of guessing, and skips compacting a definition when doing so would blow
up with a <code class="language-plaintext highlighter-rouge">NameError</code> at load time.</li>
</ul>

<p>And a couple of things happening under the hood are worth calling out:</p>

<ul>
  <li>The index now always covers your whole project, regardless of which files a
particular run happens to inspect. That means linting a single file reports the
same cross-file offenses as a full run - no more results that depend on how you
invoked RuboCop.</li>
  <li>If you want ancestry chains that reach into your gems to resolve too (think a
model inheriting from a framework base class), the new
<code class="language-plaintext highlighter-rouge">AllCops/ProjectIndexIncludesGems</code> option indexes your bundle’s sources as well.
On RuboCop’s own repo that took the share of classes with a fully resolvable
ancestry from about 20% to about 97%, at the cost of some extra memory.</li>
  <li>The language server got smarter about the index - it now builds it once per
session and reuses it across requests, rebuilding only when you save a file, so
editing a buffer doesn’t pay the indexing cost on every keystroke.</li>
</ul>

<h2 id="still-early-days">Still early days</h2>

<p>I don’t want to oversell any of this. We’re at the very beginning of RuboCop’s
cross-file journey, and there’s a lot we haven’t tackled yet. The list of checks
that would benefit from a project-wide view is long, and I suspect the most
interesting applications are still ahead of us. rubydex itself is young and
evolving, and so is our integration with it.</p>

<p>But I think it’s a genuinely promising start. A year ago the idea of RuboCop
catching a constant typo across files, or reasoning about a class’s real ancestry,
would have been a non-starter. Now it’s a config flag away.</p>

<p>If any of this sounds useful, I’d love for you to give it a try - add <code class="language-plaintext highlighter-rouge">rubydex</code> to
your <code class="language-plaintext highlighter-rouge">Gemfile</code>, flip <code class="language-plaintext highlighter-rouge">UseProjectIndex</code> on, and see what it turns up in your
codebase. And please <a href="https://github.com/rubocop/rubocop/issues">tell us how it goes</a>. The feature is still marked
experimental, and real-world feedback is exactly what we need to figure out where
to take it next. Bug reports, false positives, ideas for new cross-file cops - all
of it is welcome.</p>

<p>Big thanks to everyone who has been kicking the tires on the project index and
reporting their findings. Here’s to seeing beyond a single file.</p>

<p>Keep hacking!</p>]]></content><author><name>Bozhidar Batsov</name></author><category term="posts" /><category term="Ruby" /><category term="RuboCop" /><summary type="html"><![CDATA[RuboCop 1.89 is out, and it comes with the usual assortment of new cops and bug fixes. But there’s one theme running through this release that I’m particularly excited about, and that’s what I want to talk about today: RuboCop is finally learning to look beyond a single file.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://metaredux.com/assets/og-image.png" /><media:content medium="image" url="https://metaredux.com/assets/og-image.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>