A Clojure source structural index and form extractor, designed so an LLM can
read a .clj/.cljs/.cljc file by structure and line range instead of
by scrolling, and pull out byte-exact form text suitable as the
old_string argument to an Edit tool call.
This README is written for LLM agents. If you are one, read it once and treat
cljdx as your default lens onto Clojure code in this repo.
Read gives you raw lines. For non-trivial Clojure files this is wasteful:
- You don't know where a function starts/ends without parsing parens yourself.
- You can't tell which forms are top-level vs nested.
- When you want to
Edita specific form, you have to copy it line-by-line and hope your indentation matches exactly. If it doesn't,Editfails becauseold_stringisn't found.
cljdx gives you:
- A compact tree of forms with line ranges, depth, and a short anchor preview.
- Stable form IDs (
process-batch,process-batch.2.1,top-3.1, …) that you can reference in follow-up calls. - An
extractmode that prints the exact bytes of any form — paste it straight into anEditcall.
Rule of thumb: for any non-trivial work on .clj/.cljs/.cljc, run
cljdx FILE first instead of Read FILE.
cljdx is a babashka script. Requires bb on PATH.
Clone and symlink the launcher onto your PATH:
git clone https://github.com/techascent/cljdx ~/src/cljdx
ln -s ~/src/cljdx/bin/cljdx ~/.local/bin/cljdxThe launcher resolves its own canonical path, so the symlink works regardless
of where you invoke cljdx from.
Verify:
cljdx --helpThree things you'll do, in roughly this order.
cljdx src/myns/core.cljOutput shape (one form per line):
<id> <line-range> d<depth> <anchor> [×N if anchor not unique]
Example:
ns L1-L11 d0 (ns myns.core
profiler-controls L18-L30 d0 (defn- profiler-controls []
profiler-controls.1 L18 d1 [] [×2]
profiler-controls.2 L19-L30 d1 (let [^String status-msg (core/status)]
idis symbol-rooted at the top level (profiler-controls), and dotted-positional below it (profiler-controls.2is the 2nd child form). Anonymous top-level forms gettop-Nids.d0is top-level;d1is one level deep. The default index shows depths 0 and 1.[×2]means the anchor text occurs more than once in the file. Relevant forextract— see below.
cljdx src/myns/core.clj profiler-controlsShows the full subtree under profiler-controls to arbitrary depth. Use this
when the overview shows the form you care about but you need to see its
inner structure (let-bindings, nested branches, etc.).
cljdx extract src/myns/core.clj profiler-controls- stdout: raw bytes of the form. No line numbers, no decoration.
Byte-exact — copy this into the
old_stringof anEditcall and it will match. - stderr: one-line header:
form: profiler-controls L18-L30 312 bytes occurrences-in-file: 1
If occurrences-in-file is > 1, the form's text appears more than once in
the file and Edit will refuse to apply because old_string is ambiguous.
Either widen the snippet (include surrounding context manually) or pick a
unique parent form.
FILE may be - to read from stdin (handy with git show, etc.).
For "modify function foo in src/myns/core.clj":
cljdx src/myns/core.clj— confirmfooexists and note its range.cljdx src/myns/core.clj foo— inspect inner structure if you need to pick a sub-form.cljdx extract src/myns/core.clj foo— capture exact bytes.- Issue
Editwith the extracted text asold_stringand the rewritten form asnew_string.
This avoids the most common Clojure edit failure mode: paren/indent mismatch between what the LLM thinks the code looks like and what's on disk.
If the file has unbalanced delimiters, cljdx emits a reader error with an
indent-divergence hint — a line number where the indentation stops
making sense given the paren stack. This is often far from where the reader
finally notices the mismatch, and is usually the line you actually need to
fix.
An LLM only uses cljdx if it knows the tool exists. The most reliable way
to make that true across every project is a short blurb in your global
~/.claude/CLAUDE.md — that file is loaded into every Claude Code session
regardless of working directory. Suggested wording:
## Clojure source analysis
For any non-trivial work on `.clj`/`.cljs`/`.cljc` files, use `cljdx`:
`cljdx FILE` for a line-addressed structural index, `cljdx FILE NAME` for
the full subtree of a top-level form, `cljdx extract FILE FORM-ID` for
byte-exact form bytes suitable as `old_string` in Edit. Reader-error
output includes an indent-divergence hint pointing at the likely culprit
line. Run `cljdx --help` for details.For a single project, the same blurb in the project's CLAUDE.md works too,
but global is better — most LLM sessions don't know what tools are on
PATH unless something tells them.
Claude Code prompts on every shell invocation by default. Pre-allow cljdx
in settings.json so it runs silently.
User-level (~/.claude/settings.json):
{
"permissions": {
"allow": [
"Bash(cljdx)",
"Bash(cljdx *)"
]
}
}Bash(cljdx) covers the bare cljdx --help invocation; Bash(cljdx *)
covers all subcommands and arguments. Add both.
If you only want this for one project, put the same block in
.claude/settings.json (committed) or .claude/settings.local.json
(personal) at the project root.
bin/cljdx # launcher script (bb)
src/cljdx/main.clj # all logic
test/cljdx/ # tests
bb.edn # bb task entry: `bb cljdx ...`
Running the test suite:
cd ~/src/cljdx && bb test