A guide to using clj-kondo for Clojure code linting, including configuration, built-in linters, and writing custom hooks.
Detects Clojure code issues and enforces best practices using clj-kondo's static analyzer. It triggers automatically when you work with Clojure files to catch syntax errors, unused bindings, and other problems.
/plugin marketplace add hugoduncan/library-skills/plugin install clojure-libraries@library-skillsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
INDEX.mdQUICK_REFERENCE.mdREADME.mdSUMMARY.txtexamples.cljmetadata.ednA comprehensive guide to using clj-kondo for Clojure code linting, including configuration, built-in linters, and writing custom hooks.
clj-kondo is a fast, static analyzer and linter for Clojure code. It:
brew install clj-kondo/brew/clj-kondo
Download from GitHub Releases:
# Linux
curl -sLO https://raw.githubusercontent.com/clj-kondo/clj-kondo/master/script/install-clj-kondo
chmod +x install-clj-kondo
./install-clj-kondo
# Place in PATH
sudo mv clj-kondo /usr/local/bin/
clojure -Ttools install-latest :lib io.github.clj-kondo/clj-kondo :as clj-kondo
clojure -Tclj-kondo run :lint '"src"'
clj-kondo --version
# clj-kondo v2024.11.14
Lint a single file:
clj-kondo --lint src/myapp/core.clj
Lint a directory:
clj-kondo --lint src
Lint multiple paths:
clj-kondo --lint src test
src/myapp/core.clj:12:3: warning: unused binding x
src/myapp/core.clj:25:1: error: duplicate key :name
linting took 23ms, errors: 1, warnings: 1
Format: file:line:column: level: message
Human-readable (default):
clj-kondo --lint src
JSON (for tooling):
clj-kondo --lint src --config '{:output {:format :json}}'
EDN:
clj-kondo --lint src --config '{:output {:format :edn}}'
For better performance on subsequent runs:
clj-kondo --lint "$(clojure -Spath)" --dependencies --parallel --copy-configs
This caches analysis of dependencies and copies their configurations.
clj-kondo looks for .clj-kondo/config.edn in:
~/.config/clj-kondo/config.edn).clj-kondo/config.edn:
{:linters {:unused-binding {:level :warning}
:unused-namespace {:level :warning}
:unresolved-symbol {:level :error}
:invalid-arity {:level :error}}
:output {:pattern "{{LEVEL}} {{filename}}:{{row}}:{{col}} {{message}}"}}
:off - Disable the linter:info - Informational message:warning - Warning (default for most):error - Error (fails build)Disable specific linters:
{:linters {:unused-binding {:level :off}}}
Configure linter options:
{:linters {:consistent-alias {:aliases {clojure.string str
clojure.set set}}}}
Suppress warnings in specific namespaces:
{:linters {:unused-binding {:level :off
:exclude-ns [myapp.test-helpers]}}}
In source files:
;; Disable for entire namespace
(ns myapp.core
{:clj-kondo/config '{:linters {:unused-binding {:level :off}}}})
;; Disable for specific form
#_{:clj-kondo/ignore [:unused-binding]}
(let [x 1] 2)
;; Disable all linters for form
#_{:clj-kondo/ignore true}
(some-legacy-code)
Configurations merge in this order:
.clj-kondo/config.edn):unused-namespace - Warns about unused required namespaces
(ns myapp.core
(:require [clojure.string :as str])) ;; Warning if 'str' never used
;; Fix: Remove unused require
:unsorted-required-namespaces - Enforces sorted requires
{:linters {:unsorted-required-namespaces {:level :warning}}}
:namespace-name-mismatch - Ensures namespace matches file path
;; In src/myapp/utils.clj
(ns myapp.helpers) ;; Error: should be myapp.utils
:unused-binding - Warns about unused let bindings
(let [x 1
y 2] ;; Warning: y is unused
x)
;; Fix: Remove or prefix with underscore
(let [x 1
_y 2]
x)
:unresolved-symbol - Catches typos and undefined symbols
(defn foo []
(bar)) ;; Error: unresolved symbol bar
;; Fix: Define bar or require it
:unused-private-var - Warns about unused private definitions
(defn- helper []) ;; Warning if never called
;; Fix: Remove or use it
:invalid-arity - Catches incorrect function call arities
(defn add [a b] (+ a b))
(add 1) ;; Error: wrong arity, expected 2 args
;; Fix: Provide correct number of arguments
:missing-body-in-when - Warns about empty when blocks
(when condition) ;; Warning: missing body
;; Fix: Add body or use when-not/if
:duplicate-map-key - Catches duplicate keys in maps
{:name "Alice"
:age 30
:name "Bob"} ;; Error: duplicate key :name
:duplicate-set-key - Catches duplicate values in sets
#{1 2 1} ;; Error: duplicate set element
:misplaced-docstring - Warns about incorrectly placed docstrings
(defn foo
[x]
"This is wrong" ;; Warning: docstring after params
x)
;; Fix: Place before params
(defn foo
"This is correct"
[x]
x)
:type-mismatch - Basic type checking
(inc "string") ;; Warning: expected number
:invalid-arities - Checks arities for core functions
(map) ;; Error: map requires at least 2 arguments
Hooks are custom linting rules written in Clojure that analyze your code using clj-kondo's analysis data. They enable:
Use hooks when:
Hooks receive:
Hooks return:
.clj-kondo/
config.edn
hooks/
my_hooks.clj
.clj-kondo/hooks/my_hooks.clj:
(ns hooks.my-hooks
(:require [clj-kondo.hooks-api :as api]))
(defn my-hook
"Description of what this hook does"
[{:keys [node]}]
(let [sexpr (api/sexpr node)]
(when (some-condition? sexpr)
{:findings [{:message "Custom warning message"
:type :my-custom-warning
:row (api/row node)
:col (api/col node)}]})))
.clj-kondo/config.edn:
{:hooks {:analyze-call {my.ns/my-macro hooks.my-hooks/my-hook}}}
:analyze-call HooksTriggered when analyzing function/macro calls:
;; Hook for analyzing (deprecated-fn ...) calls
{:hooks {:analyze-call {my.api/deprecated-fn hooks.deprecation/check}}}
Hook implementation:
(defn check [{:keys [node]}]
{:findings [{:message "my.api/deprecated-fn is deprecated, use new-fn instead"
:type :deprecated-api
:row (api/row node)
:col (api/col node)
:level :warning}]})
:macroexpand HooksTransform macro calls for better analysis:
;; For macros that expand to def forms
{:hooks {:macroexpand {my.dsl/defentity hooks.dsl/expand-defentity}}}
Hook implementation:
(defn expand-defentity [{:keys [node]}]
(let [[_ name-node & body] (rest (:children node))
new-node (api/list-node
[(api/token-node 'def)
name-node
(api/map-node body)])]
{:node new-node}))
;; Get node type
(api/tag node) ;; => :list, :vector, :map, :token, etc.
;; Get children nodes
(api/children node)
;; Convert node to s-expression
(api/sexpr node)
;; Get position
(api/row node)
(api/col node)
(api/end-row node)
(api/end-col node)
;; String representation
(api/string node)
;; Create nodes
(api/token-node 'symbol)
(api/keyword-node :keyword)
(api/string-node "string")
(api/number-node 42)
(api/list-node [node1 node2 node3])
(api/vector-node [node1 node2])
(api/map-node [key-node val-node key-node val-node])
(api/set-node [node1 node2])
(api/token-node? node)
(api/keyword-node? node)
(api/string-node? node)
(api/list-node? node)
(api/vector-node? node)
(api/map-node? node)
Warn about deprecated function usage:
(ns hooks.deprecation
(:require [clj-kondo.hooks-api :as api]))
(defn warn-deprecated-fn [{:keys [node]}]
{:findings [{:message "old-api is deprecated. Use new-api instead."
:type :deprecated-function
:row (api/row node)
:col (api/col node)
:level :warning}]})
Config:
{:hooks {:analyze-call {mylib/old-api hooks.deprecation/warn-deprecated-fn}}}
Ensure specific argument types:
(ns hooks.validation
(:require [clj-kondo.hooks-api :as api]))
(defn validate-query-args [{:keys [node]}]
(let [args (rest (:children node))
first-arg (first args)]
(when-not (and first-arg (api/keyword-node? first-arg))
{:findings [{:message "First argument to query must be a keyword"
:type :invalid-argument
:row (api/row node)
:col (api/col node)
:level :error}]})))
Config:
{:hooks {:analyze-call {mylib/query hooks.validation/validate-query-args}}}
Expand custom DSL for better analysis:
(ns hooks.dsl
(:require [clj-kondo.hooks-api :as api]))
(defn expand-defrequest
"Expand (defrequest name & body) to (def name (request & body))"
[{:keys [node]}]
(let [[_ name-node & body-nodes] (:children node)
request-call (api/list-node
(list* (api/token-node 'request)
body-nodes))
expanded (api/list-node
[(api/token-node 'def)
name-node
request-call])]
{:node expanded}))
Config:
{:hooks {:macroexpand {myapp.http/defrequest hooks.dsl/expand-defrequest}}}
Warn about unsafe concurrent usage:
(ns hooks.concurrency
(:require [clj-kondo.hooks-api :as api]))
(defn check-atom-swap [{:keys [node]}]
(let [args (rest (:children node))
fn-arg (second args)]
(when (and fn-arg
(api/list-node? fn-arg)
(= 'fn (api/sexpr (first (:children fn-arg)))))
{:findings [{:message "Consider using swap-vals! for atomicity"
:type :concurrency-hint
:row (api/row node)
:col (api/col node)
:level :info}]})))
Ensure maps have required keys:
(ns hooks.maps
(:require [clj-kondo.hooks-api :as api]))
(defn validate-config-keys [{:keys [node]}]
(let [args (rest (:children node))
config-map (first args)]
(when (api/map-node? config-map)
(let [keys (->> (:children config-map)
(take-nth 2)
(map api/sexpr)
(set))
required #{:host :port :timeout}
missing (clojure.set/difference required keys)]
(when (seq missing)
{:findings [{:message (str "Missing required keys: " missing)
:type :missing-config-keys
:row (api/row node)
:col (api/col node)
:level :error}]})))))
;; test-hook.clj
(ns test-hook
(:require [mylib :as lib]))
(lib/deprecated-fn) ;; Should trigger warning
clj-kondo --lint test-hook.clj
Use clj-kondo.core for testing:
(ns hooks.my-hooks-test
(:require [clojure.test :refer [deftest is testing]]
[clj-kondo.core :as clj-kondo]))
(deftest test-my-hook
(testing "detects deprecated function usage"
(let [result (with-in-str "(ns test) (mylib/old-api)"
(clj-kondo/run!
{:lint ["-"]
:config {:hooks {:analyze-call
{mylib/old-api
hooks.deprecation/warn-deprecated-fn}}}}))]
(is (= 1 (count (:findings result))))
(is (= :deprecated-function
(-> result :findings first :type))))))
Include hooks with your library:
my-library/
.clj-kondo/
config.edn # Hook registration
hooks/
my_library.clj # Hook implementations
src/
my_library/
core.clj
Users get hooks automatically via --copy-configs.
Create a dedicated hook library:
;; deps.edn
{:paths ["."]
:deps {clj-kondo/clj-kondo {:mvn/version "2024.11.14"}}}
Users install via:
clj-kondo --lint "$(clojure -Spath -Sdeps '{:deps {my/hooks {:git/url \"...\"}}}')" --dependencies --copy-configs
Install Calva:
.clj-kondo directory recognitionWith flycheck-clj-kondo:
(use-package flycheck-clj-kondo
:ensure t)
With ALE:
let g:ale_linters = {'clojure': ['clj-kondo']}
name: Lint
on: [push, pull_request]
jobs:
clj-kondo:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install clj-kondo
run: |
curl -sLO https://raw.githubusercontent.com/clj-kondo/clj-kondo/master/script/install-clj-kondo
chmod +x install-clj-kondo
./install-clj-kondo
- name: Run clj-kondo
run: clj-kondo --lint src test
lint:
image: cljkondo/clj-kondo:latest
script:
- clj-kondo --lint src test
.git/hooks/pre-commit:
#!/bin/bash
clj-kondo --lint src test
exit $?
Begin with zero configuration - clj-kondo's defaults catch most issues.
For existing projects:
# Generate baseline
clj-kondo --lint src --config '{:output {:exclude-warnings true}}'
# Fix incrementally
Standardize via .clj-kondo/config.edn:
{:linters {:consistent-alias {:level :warning
:aliases {clojure.string str
clojure.set set}}}
:output {:exclude-files ["generated/"]}}
Write hooks for:
# Run once after dep changes
clj-kondo --lint "$(clojure -Spath)" --dependencies --parallel --copy-configs
Prefer fixing over ignoring. When ignoring:
;; Document why
#_{:clj-kondo/ignore [:unresolved-symbol]
:reason "Macro generates this symbol"}
(some-macro)
Unresolved symbol in macro:
;; Add to config
{:lint-as {myapp/my-macro clojure.core/let}}
Incorrect arity for variadic macro:
Write a macroexpand hook (see Custom Hooks section).
Slow linting:
# Cache dependencies
clj-kondo --lint "$(clojure -Spath)" --dependencies --parallel
# Exclude large dirs
{:output {:exclude-files ["node_modules/" "target/"]}}
Hook not triggering:
Hook errors:
# Run with debug output
clj-kondo --lint src --debug
Check:
.clj-kondo/config.edn (note the dot)clj-kondo is an essential tool for Clojure development offering:
Start with the defaults, customize as needed, and leverage hooks for your specific requirements.
Master authentication and authorization patterns including JWT, OAuth2, session management, and RBAC to build secure, scalable access control systems. Use when implementing auth systems, securing APIs, or debugging security issues.