Smithery Logo
MCPsSkillsDocsPricing
Login
Smithery Logo

Accelerating the Agent Economy

Resources

DocumentationPrivacy PolicySystem Status

Company

PricingAboutBlog

Connect

© 2026 Smithery. All rights reserved.

    hugoduncan

    clj-kondo

    hugoduncan/clj-kondo
    Coding
    3

    About

    SKILL.md

    Install

    Install via Skills CLI

    or add to your agent
    • Claude Code
      Claude Code
    • Codex
      Codex
    • OpenClaw
      OpenClaw
    • Cursor
      Cursor
    • Amp
      Amp
    • GitHub Copilot
      GitHub Copilot
    • Gemini CLI
      Gemini CLI
    • Kilo Code
      Kilo Code
    • Junie
      Junie
    • Replit
      Replit
    • Windsurf
      Windsurf
    • Cline
      Cline
    • Continue
      Continue
    • OpenCode
      OpenCode
    • OpenHands
      OpenHands
    • Roo Code
      Roo Code
    • Augment
      Augment
    • Goose
      Goose
    • Trae
      Trae
    • Zencoder
      Zencoder
    • Antigravity
      Antigravity
    ├─
    ├─
    └─

    About

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

    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)

    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

    • Official Documentation
    • Hook Examples
    • Configuration Reference
    • Hooks API Reference
    • Linters Reference

    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.

    Recommended Servers
    Context7
    Context7
    Repository
    hugoduncan/library-skills
    Files