# clj-kondo reads your Clojure namespaces without running them

> clj-kondo is a static analyzer for Clojure, ClojureScript and EDN, distributed as a native binary built with GraalVM, with a documented analysis output so teams can write their own linters on top. Its accuracy depends entirely on what it can resolve from the classpath you hand it, which is the trade every static analyzer makes.

**clj-kondo/clj-kondo** — Static analyzer and linter for Clojure code that sparks joy

- Repository: https://github.com/clj-kondo/clj-kondo
- Stars: 1,865 · Forks: 312
- Language: Clojure
- License: EPL-1.0
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/clj-kondo-clj-kondo

## It never executes your program, and that is the whole design

The rationale in the README is one sentence: clj-kondo performs static analysis on Clojure, ClojureScript and EDN, and informs you about potential errors while you are typing, without executing your program.

That single constraint explains the rest. An analyzer that does not run the code cannot observe a macro expansion, a dynamic var, or a value produced at runtime. What it can do is read every namespace, resolve symbols, and compare call shapes against the var metadata it found. Everything in the detection list follows from that: it is all syntactic and arity level reasoning plus name resolution.

The distribution follows from it too. The README describes the linter as compiled to native code using GraalVM, and as build tool and editor agnostic. There is a `Dockerfile` in the repository root that shows how that binary is produced, and it is not a small file: it starts from a Clojure Leiningen image, installs curl, unzip, build-essential and zlib1g-dev, downloads GraalVM CE 21.0.0 for Java 11 on Linux amd64, sets GRAALVM_HOME and JAVA_HOME, runs `./script/setup-musl` and then `./script/compile`, and copies the result into a `ubuntu:latest` stage as `/bin/clj-kondo`. A static musl build is the reason the final image can be a bare Ubuntu.

It is also published on Clojars as a JVM artifact, and the README links separate documentation for running on the JVM and for running with Docker.

## The detection list is long, and two entries are worth pausing on

The README enumerates what the linter catches. The categories: inline `def` expressions, redundant `do` and `let` wrappings, unused private vars, use of private and deprecated vars, required but unused namespaces, unsorted required namespaces, referred but unused vars, duplicate requires, unused function arguments and let bindings, unused imports, redefined vars, unresolved symbols, vars and namespaces, misplaced docstrings, duplicate map keys and set elements, duplicates and quoting in case test constants, missing map keys, invalid number of forms in binding vectors, missing assertions in `clojure.test/deftest`, alias consistency, shadowed vars, and format string argument mismatches.

Arity checking is the deepest part of the list. It works within the same namespace and across namespaces, on static Java method calls, on local `let` and `letfn` binding calls, on recursive calls including `recur`, and it reports conflicting arities in overloaded functions. The last one is a genuine bug class that a compiler rarely reaches.

Two entries are optional and therefore off by default. Marked as unused but actually used arguments and let bindings is optional, which is the classic false positive on intentionally unused bindings. Two argument usage of `reduce` is also optional, which is a style choice some teams make and others do not.

The style suggestions are separate and come from named sources: rules from Stuart Sierra's how to ns, use `:else` as the catch-all test expression in `cond`, use `seq` instead of `(not (empty? ...))`, and keep lines from getting too long. Each is linked to the reference it comes from, so a team that disagrees can find the argument rather than the assertion.

Type checking and Datalog syntax checking are listed as capabilities too, with type checking documented separately in `doc/types.md`.

## Four input shapes: stdin, a file, a directory, or a classpath

The command line accepts four different kinds of input, and the fourth is the one that determines how much the linter can resolve.

From stdin, with `-` as the filename:

```sh
echo '(def x (def x 1))' | clj-kondo --lint -
<stdin>:1:8: warning: inline def
```

For a file, the same warning is reported against the real path:

```sh
echo '(def x (def x 1))' > /tmp/foo.clj
clj-kondo --lint /tmp/foo.clj
/tmp/foo.clj:1:8: warning: inline def
```

For a directory, only `.clj`, `.cljs` and `.cljc` files are processed, and output shows both warnings and errors:

```sh
clj-kondo --lint src
src/clj_kondo/test.cljs:7:1: warning: redundant do
src/clj_kondo/calls.clj:291:3: error: Wrong number of args (1) passed to clj-kondo.calls/analyze-calls
```

And for a project classpath, which is what makes cross namespace analysis work:

```sh
clj-kondo --lint "$(lein classpath)"
```

That last line is the difference between a linter that knows `analyze-calls` exists and one that does not. A directory scan alone tells clj-kondo about the code in your repository; handing it the classpath tells it about your dependencies as well, and with them the var metadata that arity checking needs.

Two more flags shape the output. `--lang` overrides the language when the file extension cannot supply it, with supported values `clj`, `cljs` and `cljc`. `--filename` sets the reported filename when linting stdin, which matters for editors that pipe buffers and still want the right path in the diagnostic.

## Caching, config merging, and the .clj-kondo directory

Because resolution work repeats across runs, clj-kondo caches it. The default is to use the cache, resolved from the nearest `.clj-kondo` directory in the current and parent directories. `--cache false` turns that off, and `--cache-dir <dir>` points the cache somewhere else, and is ignored when the cache is disabled.

Config follows a similar layered model. `--config` takes either a file or an EDN expression and is merged as extra config on top of what is found. `--config-dir` replaces the auto-detected `.clj-kondo` directory outright rather than merging with it. The repository itself carries a `.clj-kondo/` directory at the top level, which is how the project lints its own source.

Three flags exist for working with dependencies, and they are the ones to understand before writing a CI script. `--dependencies` suppresses all findings, which makes it useful for populating the cache while walking a dependency tree. `--copy-configs` copies the configs found in those dependencies, so a library's own lint settings can travel with it. `--skip-lint` runs those other tasks such as copy-configs without doing any analysis at all.

That trio exists because a linter that reads third party code has to decide what to do with the config found inside it. Copying configs gives you the library author's view, which is sometimes what you want and sometimes a source of surprise findings you did not write.

Exit behaviour is controlled by `--fail-level`, which takes `warning` or `error` and defaults to `warning`, meaning a single warning fails a build by default. `--parallel` lints sources in parallel.

## Macro support is an explicit list, and the analysis output is the extension point

Since the analyzer never runs your code, macro syntax has to be taught to it. The README names the ones it handles: `clojure.core.async/alt!!`, `schema.core/defn` and `potemkin/import-vars`.

The README gives three examples of commonly used macro syntax it supports. A macro that transforms its arguments into something clj-kondo cannot model will be analysed as a function call with the arguments you wrote, which is sometimes right and sometimes not.

The answer to that gap is the second half of the design. The README states that clj-kondo provides analysis data so you can build your own custom linters, with the format documented in `analysis/` and the API in `API.md` at the repository root. So the intended path for a project with unusual macros is to run clj-kondo for everything it understands and add a linter of your own over its output for the rest.

Beyond the core language, clj-kondo also detects common errors in `deps.edn` and `bb.edn`, which puts it in front of the build files themselves and not only the sources they select.

The full linter catalogue is in `doc/linters.md`. The repository also has an interactive playground at clj-kondo.michielborkent.nl, which is the fastest way to find out how a given form is treated before you commit a config for it.

## EPL-1.0, frequent dated releases, and what CI it plugs into

The project is EPL-1.0, with the licence at the root of the repository. It is used as a build step in most projects that adopt it, so the file level copyleft question is worth reading before you vendor it into a proprietary codebase.

Release cadence is dated rather than semantic. The recent versions are v2026.08.04, v2026.08.03 and v2026.07.24, and the help output shown in the README prints its own version as `clj-kondo v2024.11.14`. A calendar version means a breaking change can arrive in any release, so pinning in a build script matters more here than it would with a semver tag.

The last push was on 2026-10-15, so the project is being worked on.

The repository ships the pieces for each environment it claims to support: a `Dockerfile`, a `Dockerfile.alpine` for the musl build, a `Dockerfile.ci`, and a `snapcraft.yaml` for a Snap package. There is a `.pre-commit-hooks.yaml` at the root, which is the wiring a team needs to run it as a pre-commit hook without writing a wrapper, and a `.dir-locals.el` for Emacs projects.

Continuous integration is configured twice over, with CircleCI directories at the root and a `.cirrus.yml` for Cirrus CI, plus an AppVeyor badge in the README for the Windows path.

## Where a compile step or an editor integration fits instead

In exchange you need the toolchain running, a clean build directory, and a build slow enough that a per keystroke loop is not practical.

The other alternative is a lighter binary linter that resolves nothing. A Clojure dialect interpreter that reads a file and checks for basic errors will start faster and lint a directory in a fraction of the time, and it will tell you nothing about an arity error against a function in a library you have not loaded. Choosing that trade is choosing syntax checking over analysis.

Inside an editor, the choice is between an editor plugin driving the binary and the editor's own Clojure tooling. The README describes clj-kondo as editor agnostic, which means it deliberately stays out of that argument: it is a process you call, and the plugin is someone else's problem.

The realistic combination is the compiler for correctness in a full build, clj-kondo for the fast feedback loop and the CI gate, and the analysis output when a project specific rule has to cross the gap between them.

## Conclusion

Adopt clj-kondo if you want arity and unused-binding checks and can make classpath resolution reproducible in CI. Do not adopt it as your only gate without checking how it handles the macros used by your project. Verify first that `clj-kondo --lint "$(lein classpath)"` resolves the namespaces you expected in your own build, and set `--fail-level error` rather than accepting the default warning before wiring it into a blocking CI step.

## FAQ

### What file types does clj-kondo lint?

It is compatible with `.clj`, `.cljs`, `.cljc` and `.edn` files, and when linting a directory or classpath only `.clj`, `.cljs` and `.cljc` files are processed.

### How do I make clj-kondo lint a whole project including dependencies?

Run `clj-kondo --lint "$(lein classpath)"` so the analyzer receives the project classpath, which is what gives it the var metadata needed for cross namespace arity checks.

### Which macros does clj-kondo understand?

The README names support for the syntax of `clojure.core.async/alt!!`, `schema.core/defn` and `potemkin/import-vars`. Anything else is analysed as a plain function call.

### Does clj-kondo fail a build on warnings by default?

Yes. The `--fail-level` option defaults to `warning`, and the supported values are `warning` and `error`.

### What licence is clj-kondo released under?

EPL-1.0, with the licence text in the LICENSE file at the repository root.

## Sources

- [clj-kondo/clj-kondo on GitHub](https://github.com/clj-kondo/clj-kondo)
- [Issues](https://github.com/clj-kondo/clj-kondo/issues)
- [License: EPL-1.0](https://github.com/clj-kondo/clj-kondo/blob/master/LICENSE)
- [README](https://github.com/clj-kondo/clj-kondo/blob/master/README.md)
- [Releases](https://github.com/clj-kondo/clj-kondo/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/clj-kondo-clj-kondo
