Claude-skill-registry clj-kondo
A guide to using clj-kondo for Clojure code linting, including configuration, built-in linters, and writing custom hooks.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/clj-kondo" ~/.claude/skills/majiayu000-claude-skill-registry-clj-kondo && rm -rf "$T"
skills/data/clj-kondo/SKILL.mdclj-kondo Skill Guide
A comprehensive guide to using clj-kondo for Clojure code linting, including configuration, built-in linters, and writing custom hooks.
Table of Contents
- Introduction
- Installation
- Getting Started
- Configuration
- Built-in Linters
- Custom Hooks
- IDE Integration
- CI/CD Integration
- Best Practices
- Troubleshooting
Introduction
What is clj-kondo?
clj-kondo is a fast, static analyzer and linter for Clojure code. It:
- Catches syntax errors and common mistakes
- Enforces code style and best practices
- Provides immediate feedback during development
- Supports custom linting rules via hooks
- Integrates with all major editors and CI systems
- Requires no project dependencies or runtime
Why Use clj-kondo?
- Fast: Native binary with instant startup
- Accurate: Deep understanding of Clojure semantics
- Extensible: Custom hooks for domain-specific rules
- Zero configuration: Works out of the box
- Cross-platform: Native binaries for Linux, macOS, Windows
- IDE integration: Works with Emacs, VS Code, IntelliJ, Vim, etc.
Installation
macOS/Linux (Homebrew)
brew install clj-kondo/brew/clj-kondo
Manual Binary Installation
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/
Via Clojure CLI
clojure -Ttools install-latest :lib io.github.clj-kondo/clj-kondo :as clj-kondo clojure -Tclj-kondo run :lint '"src"'
Verify Installation
clj-kondo --version # clj-kondo v2024.11.14
Getting Started
Basic Usage
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
Understanding Output
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
Output Formats
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}}'
Creating Cache
For better performance on subsequent runs:
clj-kondo --lint "$(clojure -Spath)" --dependencies --parallel --copy-configs
This caches analysis of dependencies and copies their configurations.
Configuration
Configuration File Location
clj-kondo looks for
.clj-kondo/config.edn in:
- Current directory
- Parent directories (walking up)
- Home directory (
)~/.config/clj-kondo/config.edn
Basic Configuration
.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}}"}}
Linter Levels
- Disable the linter:off
- Informational message:info
- Warning (default for most):warning
- Error (fails build):error
Global Configuration
Disable specific linters:
{:linters {:unused-binding {:level :off}}}
Configure linter options:
{:linters {:consistent-alias {:aliases {clojure.string str clojure.set set}}}}
Local Configuration
Suppress warnings in specific namespaces:
{:linters {:unused-binding {:level :off :exclude-ns [myapp.test-helpers]}}}
Inline Configuration
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)
Configuration Merging
Configurations merge in this order:
- Built-in defaults
- Home directory config
- Project config (
).clj-kondo/config.edn - Inline metadata
Built-in Linters
Namespace and Require Linters
- Warns about unused required namespaces:unused-namespace
(ns myapp.core (:require [clojure.string :as str])) ;; Warning if 'str' never used ;; Fix: Remove unused require
- Enforces sorted requires:unsorted-required-namespaces
{:linters {:unsorted-required-namespaces {:level :warning}}}
- Ensures namespace matches file path:namespace-name-mismatch
;; In src/myapp/utils.clj (ns myapp.helpers) ;; Error: should be myapp.utils
Binding and Symbol Linters
- Warns about unused let bindings:unused-binding
(let [x 1 y 2] ;; Warning: y is unused x) ;; Fix: Remove or prefix with underscore (let [x 1 _y 2] x)
- Catches typos and undefined symbols:unresolved-symbol
(defn foo [] (bar)) ;; Error: unresolved symbol bar ;; Fix: Define bar or require it
- Warns about unused private definitions:unused-private-var
(defn- helper []) ;; Warning if never called ;; Fix: Remove or use it
Function and Arity Linters
- Catches incorrect function call arities:invalid-arity
(defn add [a b] (+ a b)) (add 1) ;; Error: wrong arity, expected 2 args ;; Fix: Provide correct number of arguments
- Warns about empty when blocks:missing-body-in-when
(when condition) ;; Warning: missing body ;; Fix: Add body or use when-not/if
Collection and Syntax Linters
- Catches duplicate keys in maps:duplicate-map-key
{:name "Alice" :age 30 :name "Bob"} ;; Error: duplicate key :name
- Catches duplicate values in sets:duplicate-set-key
#{1 2 1} ;; Error: duplicate set element
- Warns about incorrectly placed docstrings:misplaced-docstring
(defn foo [x] "This is wrong" ;; Warning: docstring after params x) ;; Fix: Place before params (defn foo "This is correct" [x] x)
Type and Spec Linters
- Basic type checking:type-mismatch
(inc "string") ;; Warning: expected number
- Checks arities for core functions:invalid-arities
(map) ;; Error: map requires at least 2 arguments
Custom Hooks
What Are Hooks?
Hooks are custom linting rules written in Clojure that analyze your code using clj-kondo's analysis data. They enable:
- Domain-specific linting rules
- API usage validation
- Deprecation warnings
- Team convention enforcement
- Advanced static analysis
When to Use Hooks
Use hooks when:
- Built-in linters don't cover your needs
- You have library-specific patterns to enforce
- You want to warn about deprecated APIs
- You need to validate domain-specific logic
- You want to enforce team coding standards
Hook Architecture
Hooks receive:
- Analysis context - Information about the code being analyzed
- Node - The AST node being examined
Hooks return:
- Findings - Lint warnings/errors to report
- Updated analysis - Modified context for downstream analysis
Creating Your First Hook
1. Hook Directory Structure
.clj-kondo/ config.edn hooks/ my_hooks.clj
2. Basic Hook Template
.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)}]})))
3. Register Hook
.clj-kondo/config.edn:
{:hooks {:analyze-call {my.ns/my-macro hooks.my-hooks/my-hook}}}
Hook Types
:analyze-call
Hooks
:analyze-callTriggered 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
Hooks
:macroexpandTransform 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}))
Hook API Reference
Node Functions
;; 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)
Node Constructors
;; 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])
Node Predicates
(api/token-node? node) (api/keyword-node? node) (api/string-node? node) (api/list-node? node) (api/vector-node? node) (api/map-node? node)
Practical Hook Examples
Example 1: Deprecation Warning
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}}}
Example 2: Argument Validation
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}}}
Example 3: DSL Expansion
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}}}
Example 4: Thread-Safety Check
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}]})))
Example 5: Required Keys Validation
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}]})))))
Testing Hooks
Manual Testing
- Create test file:
;; test-hook.clj (ns test-hook (:require [mylib :as lib])) (lib/deprecated-fn) ;; Should trigger warning
- Run clj-kondo:
clj-kondo --lint test-hook.clj
Unit Testing Hooks
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))))))
Distributing Hooks
As Library Config
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.
Standalone Hook Library
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
Hook Best Practices
- Performance: Keep hooks fast - they run on every lint
- Specificity: Target specific forms, not every function call
- Clear messages: Provide actionable error messages
- Documentation: Document what hooks check and why
- Testing: Test hooks with various inputs
- Versioning: Version hooks with your library
- Graceful degradation: Handle malformed code gracefully
IDE Integration
VS Code
Install Calva:
- clj-kondo linting enabled by default
- Real-time feedback as you type
- Automatic
directory recognition.clj-kondo
Emacs
With
flycheck-clj-kondo:
(use-package flycheck-clj-kondo :ensure t)
IntelliJ IDEA / Cursive
- Native clj-kondo integration
- Configure via Preferences → Editor → Inspections
Vim/Neovim
With ALE:
let g:ale_linters = {'clojure': ['clj-kondo']}
CI/CD Integration
GitHub Actions
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
GitLab CI
lint: image: cljkondo/clj-kondo:latest script: - clj-kondo --lint src test
Pre-commit Hook
.git/hooks/pre-commit:
#!/bin/bash clj-kondo --lint src test exit $?
Best Practices
1. Start with Defaults
Begin with zero configuration - clj-kondo's defaults catch most issues.
2. Gradual Adoption
For existing projects:
# Generate baseline clj-kondo --lint src --config '{:output {:exclude-warnings true}}' # Fix incrementally
3. Team Configuration
Standardize via
.clj-kondo/config.edn:
{:linters {:consistent-alias {:level :warning :aliases {clojure.string str clojure.set set}}} :output {:exclude-files ["generated/"]}}
4. Leverage Hooks for Domain Logic
Write hooks for:
- API deprecations
- Team conventions
- Domain-specific validations
5. Cache Dependencies
# Run once after dep changes clj-kondo --lint "$(clojure -Spath)" --dependencies --parallel --copy-configs
6. Ignore Thoughtfully
Prefer fixing over ignoring. When ignoring:
;; Document why #_{:clj-kondo/ignore [:unresolved-symbol] :reason "Macro generates this symbol"} (some-macro)
Troubleshooting
False Positives
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).
Performance Issues
Slow linting:
# Cache dependencies clj-kondo --lint "$(clojure -Spath)" --dependencies --parallel # Exclude large dirs {:output {:exclude-files ["node_modules/" "target/"]}}
Hook Debugging
Hook not triggering:
- Check hook registration in config.edn
- Verify namespace matches
- Test with minimal example
- Check for typos in qualified symbols
Hook errors:
# Run with debug output clj-kondo --lint src --debug
Configuration Not Loading
Check:
- File is named
(note the dot).clj-kondo/config.edn - EDN syntax is valid
- File is in project root or parent directory
Resources
Summary
clj-kondo is an essential tool for Clojure development offering:
- Immediate feedback on code quality
- Extensive built-in linting rules
- Powerful custom hooks for domain-specific rules
- Seamless IDE and CI/CD integration
- Zero-configuration operation with extensive customization options
Start with the defaults, customize as needed, and leverage hooks for your specific requirements.