Claude Code Plugins

Community-maintained marketplace

Feedback

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

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

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

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)

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:

  1. Current directory
  2. Parent directories (walking up)
  3. 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

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

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:

  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

(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

Binding and Symbol Linters

: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

Function and Arity Linters

: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

Collection and Syntax Linters

: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 and Spec Linters

: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

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:

(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

Triggered 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

Transform 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

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

(lib/deprecated-fn) ;; Should trigger warning
  1. 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

  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:

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

  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:

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