Agent skill

clj-kondo

A guide to using clj-kondo for Clojure code linting, including configuration, built-in linters, and writing custom hooks.

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/development/clj-kondo-hugoduncan-library-skills

SKILL.md

clj-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

  1. Introduction
  2. Installation
  3. Getting Started
  4. Configuration
  5. Built-in Linters
  6. Custom Hooks
  7. IDE Integration
  8. CI/CD Integration
  9. Best Practices
  10. 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)

bash
brew install clj-kondo/brew/clj-kondo

Manual Binary Installation

Download from GitHub Releases:

bash
# 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

bash
clojure -Ttools install-latest :lib io.github.clj-kondo/clj-kondo :as clj-kondo
clojure -Tclj-kondo run :lint '"src"'

Verify Installation

bash
clj-kondo --version
# clj-kondo v2024.11.14

Getting Started

Basic Usage

Lint a single file:

bash
clj-kondo --lint src/myapp/core.clj

Lint a directory:

bash
clj-kondo --lint src

Lint multiple paths:

bash
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):

bash
clj-kondo --lint src

JSON (for tooling):

bash
clj-kondo --lint src --config '{:output {:format :json}}'

EDN:

bash
clj-kondo --lint src --config '{:output {:format :edn}}'

Creating Cache

For better performance on subsequent runs:

bash
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:

  1. Current directory
  2. Parent directories (walking up)
  3. Home directory (~/.config/clj-kondo/config.edn)

Basic Configuration

.clj-kondo/config.edn:

clojure
{: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

  • :off - Disable the linter
  • :info - Informational message
  • :warning - Warning (default for most)
  • :error - Error (fails build)

Global Configuration

Disable specific linters:

clojure
{:linters {:unused-binding {:level :off}}}

Configure linter options:

clojure
{:linters {:consistent-alias {:aliases {clojure.string str
                                        clojure.set set}}}}

Local Configuration

Suppress warnings in specific namespaces:

clojure
{:linters {:unused-binding {:level :off
                            :exclude-ns [myapp.test-helpers]}}}

Inline Configuration

In source files:

clojure
;; 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:

  1. Built-in defaults
  2. Home directory config
  3. Project config (.clj-kondo/config.edn)
  4. Inline metadata

Built-in Linters

Namespace and Require Linters

:unused-namespace - Warns about unused required namespaces

clojure
(ns myapp.core
  (:require [clojure.string :as str])) ;; Warning if 'str' never used

;; Fix: Remove unused require

:unsorted-required-namespaces - Enforces sorted requires

clojure
{:linters {:unsorted-required-namespaces {:level :warning}}}

:namespace-name-mismatch - Ensures namespace matches file path

clojure
;; In src/myapp/utils.clj
(ns myapp.helpers) ;; Error: should be myapp.utils

Binding and Symbol Linters

:unused-binding - Warns about unused let bindings

clojure
(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

clojure
(defn foo []
  (bar)) ;; Error: unresolved symbol bar

;; Fix: Define bar or require it

:unused-private-var - Warns about unused private definitions

clojure
(defn- helper []) ;; Warning if never called

;; Fix: Remove or use it

Function and Arity Linters

:invalid-arity - Catches incorrect function call arities

clojure
(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

clojure
(when condition) ;; Warning: missing body

;; Fix: Add body or use when-not/if

Collection and Syntax Linters

:duplicate-map-key - Catches duplicate keys in maps

clojure
{:name "Alice"
 :age 30
 :name "Bob"} ;; Error: duplicate key :name

:duplicate-set-key - Catches duplicate values in sets

clojure
#{1 2 1} ;; Error: duplicate set element

:misplaced-docstring - Warns about incorrectly placed docstrings

clojure
(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

:type-mismatch - Basic type checking

clojure
(inc "string") ;; Warning: expected number

:invalid-arities - Checks arities for core functions

clojure
(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:

  1. Analysis context - Information about the code being analyzed
  2. Node - The AST node being examined

Hooks return:

  1. Findings - Lint warnings/errors to report
  2. 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:

clojure
(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:

clojure
{:hooks {:analyze-call {my.ns/my-macro hooks.my-hooks/my-hook}}}

Hook Types

:analyze-call Hooks

Triggered when analyzing function/macro calls:

clojure
;; Hook for analyzing (deprecated-fn ...) calls
{:hooks {:analyze-call {my.api/deprecated-fn hooks.deprecation/check}}}

Hook implementation:

clojure
(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

Transform macro calls for better analysis:

clojure
;; For macros that expand to def forms
{:hooks {:macroexpand {my.dsl/defentity hooks.dsl/expand-defentity}}}

Hook implementation:

clojure
(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

clojure
;; 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

clojure
;; 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

clojure
(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:

clojure
(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:

clojure
{:hooks {:analyze-call {mylib/old-api hooks.deprecation/warn-deprecated-fn}}}

Example 2: Argument Validation

Ensure specific argument types:

clojure
(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:

clojure
{:hooks {:analyze-call {mylib/query hooks.validation/validate-query-args}}}

Example 3: DSL Expansion

Expand custom DSL for better analysis:

clojure
(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:

clojure
{:hooks {:macroexpand {myapp.http/defrequest hooks.dsl/expand-defrequest}}}

Example 4: Thread-Safety Check

Warn about unsafe concurrent usage:

clojure
(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:

clojure
(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

  1. Create test file:
clojure
;; test-hook.clj
(ns test-hook
  (:require [mylib :as lib]))

(lib/deprecated-fn) ;; Should trigger warning
  1. Run clj-kondo:
bash
clj-kondo --lint test-hook.clj

Unit Testing Hooks

Use clj-kondo.core for testing:

clojure
(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:

clojure
;; deps.edn
{:paths ["."]
 :deps {clj-kondo/clj-kondo {:mvn/version "2024.11.14"}}}

Users install via:

bash
clj-kondo --lint "$(clojure -Spath -Sdeps '{:deps {my/hooks {:git/url \"...\"}}}')" --dependencies --copy-configs

Hook Best Practices

  1. Performance: Keep hooks fast - they run on every lint
  2. Specificity: Target specific forms, not every function call
  3. Clear messages: Provide actionable error messages
  4. Documentation: Document what hooks check and why
  5. Testing: Test hooks with various inputs
  6. Versioning: Version hooks with your library
  7. 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 .clj-kondo directory recognition

Emacs

With flycheck-clj-kondo:

elisp
(use-package flycheck-clj-kondo
  :ensure t)

IntelliJ IDEA / Cursive

  • Native clj-kondo integration
  • Configure via Preferences → Editor → Inspections

Vim/Neovim

With ALE:

vim
let g:ale_linters = {'clojure': ['clj-kondo']}

CI/CD Integration

GitHub Actions

yaml
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

yaml
lint:
  image: cljkondo/clj-kondo:latest
  script:
    - clj-kondo --lint src test

Pre-commit Hook

.git/hooks/pre-commit:

bash
#!/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:

bash
# Generate baseline
clj-kondo --lint src --config '{:output {:exclude-warnings true}}'

# Fix incrementally

3. Team Configuration

Standardize via .clj-kondo/config.edn:

clojure
{: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

bash
# Run once after dep changes
clj-kondo --lint "$(clojure -Spath)" --dependencies --parallel --copy-configs

6. Ignore Thoughtfully

Prefer fixing over ignoring. When ignoring:

clojure
;; Document why
#_{:clj-kondo/ignore [:unresolved-symbol]
   :reason "Macro generates this symbol"}
(some-macro)

Troubleshooting

False Positives

Unresolved symbol in macro:

clojure
;; 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:

bash
# Cache dependencies
clj-kondo --lint "$(clojure -Spath)" --dependencies --parallel

# Exclude large dirs
{:output {:exclude-files ["node_modules/" "target/"]}}

Hook Debugging

Hook not triggering:

  1. Check hook registration in config.edn
  2. Verify namespace matches
  3. Test with minimal example
  4. Check for typos in qualified symbols

Hook errors:

bash
# Run with debug output
clj-kondo --lint src --debug

Configuration Not Loading

Check:

  1. File is named .clj-kondo/config.edn (note the dot)
  2. EDN syntax is valid
  3. 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.

Didn't find tool you were looking for?

Be as detailed as possible for better results