smithery.ai

clj-kondo

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

First seen Apr 4, 2026

Installation

$ npx skills add https://smithery.ai

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery.ai · top by installs.

npx skills add https://smithery.ai

Browse all from smithery.ai

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Skill metadata

Parsed from SKILL.md frontmatter.

Declared agents vscode

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 19,808 B
  • docs SUMMARY.md 138 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 1 installs

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](#introduction)
  2. [Installation](#installation)
  3. [Getting Started](#getting-started)
  4. [Configuration](#configuration)
  5. [Built-in Linters](#built-in-linters)
  6. [Custom Hooks](#custom-hooks)
  7. [IDE Integration](#ide-integration)
  8. [CI/CD Integration](#cicd-integration)
  9. [Best Practices](#best-practices)
  10. [Troubleshooting](#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.