Skip to content

Instantly share code, notes, and snippets.

@aygp-dr
Last active May 23, 2026 13:26
Show Gist options
  • Select an option

  • Save aygp-dr/c934edac2bba4683e5bb06b6ddeab041 to your computer and use it in GitHub Desktop.

Select an option

Save aygp-dr/c934edac2bba4683e5bb06b6ddeab041 to your computer and use it in GitHub Desktop.
nREPL Bencode Protocol Experience Report - Direct protocol communication from Babashka/Clojure

Clojure Projects for nREPL Skill Application

deps.edn Projects (JVM Clojure - Full nREPL)

These can use the full JVM nREPL with all middleware:

Project Description
jwalsh/Agent-Monitor-Console Agent monitoring
jwalsh/clojure-brain-teasers-review Clojure puzzles
jwalsh/CortexIntelligence ML/Cortex
aygp-dr/liquid-neural-networks Neural nets
aygp-dr/advent-of-code AoC solutions
aygp-dr/amc Agent Monitor Console
aygp-dr/core-async-flow-workshop core.async
aygp-dr/diairesis Division/categorization
aygp-dr/flight-tracking Current project
aygp-dr/reversible-meta-synthesis Reversible computing
aygp-dr/petclinic-lab Spring PetClinic
aygp-dr/claude-log-stream Claude log streaming
aygp-dr/lemmata Vocabulary/terms
aygp-dr/horner-fold Horner's method

Quick Setup for deps.edn Projects

# Add to deps.edn :aliases
:dev {:extra-deps {nrepl/nrepl {:mvn/version "1.3.0"}}}

# Start nREPL on LAN
clj -M:dev -m nrepl.cmdline --bind 0.0.0.0 --port 1667 &

# Connect from laptop
# Emacs: M-x cider-connect RET <host> RET 1667

project.clj Projects (Leiningen)

Project Description
jwalsh/advent-of-code-2021 AoC 2021
jwalsh/aif-c01 AI Foundations
jwalsh/advent-of-code-2023 AoC 2023
aygp-dr/squiggleconf-2025 SquiggleConf
aygp-dr/gemini-repl-002 Gemini REPL
aygp-dr/aif-c01 AI Foundations
aygp-dr/gemini-repl-004 Gemini REPL

Quick Setup for Leiningen Projects

# profiles.clj or project.clj
:repl-options {:host "0.0.0.0" :port 1667}

# Start
lein repl :headless

bb.edn Only (Babashka - Limited nREPL)

These use Babashka's nREPL (no TLS, limited middleware):

Project Description
aygp-dr/code-perf-profiler Perf profiling
aygp-dr/secure-code-analyzer Security analysis
aygp-dr/code-style-guide Style checking
aygp-dr/code-readability-analyzer Readability
aygp-dr/distributed-log-analyzer Log analysis
aygp-dr/doc-quality-checker Doc quality
aygp-dr/code-smell-detector Code smells
aygp-dr/code-explanation-gen Code explanation
aygp-dr/data-sync-verifier Data sync
aygp-dr/deprecation-detective Deprecation

Quick Setup for Babashka Projects

# Start socket REPL (simplest)
bb --socket-repl 0.0.0.0:1666

# Or nREPL (limited)
bb --nrepl-server 0.0.0.0:1667

# Connect
rlwrap nc <host> 1666  # socket REPL

Skill Installation

# Copy skill to global skills
cp -r skills/clojure-repl ~/.claude/skills/

# Or install to specific project
mkdir -p <project>/skills
cp -r skills/clojure-repl <project>/skills/

nREPL Bencode Protocol Experience Report

1 Overview

Direct nREPL protocol communication from Babashka, bypassing tmux for agentic REPL-driven development. Demonstrates raw Bencode encoding/decoding.

2 Environment

ComponentVersion
Babashka1.12.218
babashka.nrepl0.0.6-SNAPSHOT
Hosthydra (FreeBSD 14.3)

3 Bencode Protocol

nREPL uses Bencode (from BitTorrent) for message serialization.

3.1 Format

TypeEncodingExample
Integeri<num>ei42e → 42
String<len>:<chars>5:hello → “hello”
Listl<items>eli1ei2ee → [1, 2]
Dictd<key><value>...ed3:foo3:bare → {“foo”: “bar”}

3.2 Clojure Encoder

(defn bencode [data]
  (cond
    (integer? data) (str "i" data "e")
    (string? data) (str (count (.getBytes data "UTF-8")) ":" data)
    (keyword? data) (bencode (name data))
    (sequential? data) (str "l" (apply str (map bencode data)) "e")
    (map? data) (str "d"
                     (apply str (mapcat (fn [[k v]]
                                          [(bencode k) (bencode v)])
                                        (sort-by key data)))
                     "e")))

3.3 Clojure Decoder

(declare bdecode-from)

(defn read-string-bytes [^java.io.PushbackInputStream in len]
  (let [buf (byte-array len)]
    (.read in buf)
    (String. buf "UTF-8")))

(defn bdecode-from [^java.io.PushbackInputStream in]
  (let [c (.read in)]
    (cond
      ;; Integer: i<num>e
      (= c (int \i))
      (loop [sb (StringBuilder.) ch (.read in)]
        (if (= ch (int \e))
          (Long/parseLong (.toString sb))
          (recur (.append sb (char ch)) (.read in))))

      ;; List: l<items>e
      (= c (int \l))
      (loop [acc []]
        (let [peek (.read in)]
          (if (= peek (int \e))
            acc
            (do (.unread in peek)
                (recur (conj acc (bdecode-from in)))))))

      ;; Dict: d<key><value>...e
      (= c (int \d))
      (loop [acc {}]
        (let [peek (.read in)]
          (if (= peek (int \e))
            acc
            (do (.unread in peek)
                (recur (assoc acc (bdecode-from in) (bdecode-from in)))))))

      ;; String: <len>:<chars>
      :else
      (loop [sb (StringBuilder. (str (char c))) ch (.read in)]
        (if (= ch (int \:))
          (read-string-bytes in (Long/parseLong (.toString sb)))
          (recur (.append sb (char ch)) (.read in)))))))

(defn bdecode [s]
  (bdecode-from
    (java.io.PushbackInputStream.
      (java.io.ByteArrayInputStream. (.getBytes s)))))

4 Protocol Operations

4.1 describe

Get server capabilities.

>>> {"op" "describe"}
    Bencode: d2:op8:describee

<<< {"id" "unknown",
     "ops" {"clone" {}, "close" {}, "eval" {}, "describe" {}, ...},
     "session" "none",
     "status" ["done"],
     "versions" {"babashka" "1.12.218"}}

4.2 clone

Create a session. Required before eval.

>>> {"op" "clone"}
    Bencode: d2:op5:clonee

<<< {"new-session" "4b9d0490-4311-4c5b-9720-b91e4e1e1905",
     "status" ["done"]}

4.3 eval

Evaluate code in a session.

>>> {"op" "eval", "code" "(+ 1 2 3)", "session" "<id>"}
    Bencode: d4:code9:(+ 1 2 3)2:op4:eval7:session36:<id>e

<<< {"value" "6", "ns" "user", "session" "<id>"}
<<< {"status" ["done"], "session" "<id>"}

Multiple response messages until "status" ["done"].

4.4 eval with stdout

Output comes as separate out messages.

>>> {"op" "eval", "code" "(println \"Hello\") :done", "session" "<id>"}

<<< {"out" "Hello\n", "session" "<id>"}
<<< {"value" "nil", "ns" "user", "session" "<id>"}
<<< {"value" ":done", "ns" "user", "session" "<id>"}
<<< {"status" ["done"], "session" "<id>"}

4.5 close

End a session.

>>> {"op" "close", "session" "<id>"}

<<< {"status" ["done" "session-closed"], "session" "<id>"}

5 Practical Example: Live AeroAPI Query

;; Session already established

;; Load AeroAPI
(nrepl-eval sock session "(require '[aeroapi.core :as api])")
(nrepl-eval sock session "(def client (api/from-env))")

;; Query KBOS departures
(nrepl-eval sock session "
  (let [resp (api/request client
               {:method :get
                :path \"/airports/KBOS/flights/departures\"
                :query-params {:max_pages 1}})]
    {:count (count (:departures resp))
     :flights (->> (:departures resp)
                   (take 3)
                   (map #(select-keys % [:ident :destination :status])))})")

Result:

{:count 15,
 :flights ({:ident "RPA5647",
            :destination {:code "KCLT", :city "Charlotte"},
            :status "En Route / On Time"}
           {:ident "DAL456",
            :destination {:code "KATL", :city "Atlanta"},
            :status "En Route / On Time"}
           {:ident "FFT3057",
            :destination {:code "KRDU", :city "Raleigh/Durham"},
            :status "En Route / On Time"})}

6 Gotchas & Learnings

6.1 TLS Dependencies in nrepl.core

The nrepl/nrepl library requires TLS classes unavailable in Babashka:

Unable to resolve classname: java.security.cert.Certificate
Location: nrepl/tls.clj:14:3

Workaround: Speak raw Bencode protocol instead of using nrepl.core.

6.2 Babashka nREPL vs JVM nREPL

Featurebb –nrepl-serverclj -M:nrepl
Startup~10ms~3000ms
TLSNoYes
MiddlewareLimitedFull
test.checkNoYes
spec/genNoYes

For agentic workflows, Babashka nREPL is sufficient for most eval operations. For full Clojure features, use JVM nREPL.

6.3 Session State Persists

Variables defined in one eval are available in subsequent evals:

(nrepl-eval sock session "(def x 42)")
;; => #'user/x

(nrepl-eval sock session "(* x x)")
;; => 1764

6.4 String Length is Byte Length

Bencode string length is byte count, not character count:

;; WRONG for unicode
(str (count s) ":" s)

;; CORRECT
(str (count (.getBytes s "UTF-8")) ":" s)

6.5 Multiple Response Messages

Eval returns multiple messages; collect until status contains "done":

(defn nrepl-recv-all [sock]
  (loop [responses []]
    (let [resp (recv-one sock)
          responses (conj responses resp)]
      (if (some #{"done" "error"} (get resp "status"))
        responses
        (recur responses)))))

6.6 Dict Keys Must Be Sorted

Bencode spec requires dict keys in sorted order:

;; CORRECT
(sort-by key data)  ; Sort before encoding

;; Will work but technically non-compliant
;; (just iterate without sorting)

7 Files Created

FilePurpose
scripts/nrepl-client.bbFull nREPL client (unused due to TLS)
bin/repl-session.shtmux-based fallback
skills/clojure-repl/references/nrepl-protocol.mdProtocol reference
docs/agentic-repl-workflows.orgComparison of approaches

8 Conclusion

Direct Bencode communication with nREPL is feasible from Babashka for agentic workflows. The protocol is simple and stateful (sessions persist). For full Clojure features (spec generators, test.check), use JVM nREPL.

9 Local Variables

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment