clojure-programmer — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited clojure-programmer (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Target Clojure 1.12.3+ (current stable as of September 2025). Version history at https://github.com/clojure/clojure/blob/master/changes.md.
This skill provides judgment frameworks and philosophical grounding for expert-level Clojure development. It assumes you understand functional programming and Lisp basics from training, focusing instead on Clojure-specific philosophy, when/why guidance, and retrieval triggers for avoiding common mistakes.
<core_philosophy>
For foundational software engineering principles, see the software-engineer skill. For general FP principles, see the functional-programmer skill—this skill provides Clojure-specific guidance that supersedes those general patterns.
Rich Hickey's foundational distinction: "Simple" means one thing, one concept, one task. "Easy" means familiar, near at hand. Always choose simple over easy.
This philosophy permeates every Clojure decision:
The critical insight: Simplicity enables change. Easy rots into complexity. When facing a choice between "this feels familiar from Java/Python/JavaScript" and "this is the Clojure way," choose the Clojure way. The unfamiliarity is temporary; the simplicity compounds.
"Programs must be written for people to read, and only incidentally for machines to execute." — Hal Abelson, SICP
From Rich Hickey's Spec-ulation: Clojure's design was inspired by RDF's open-world assumption. Code is an ontology of a solution. Names create enduring semantic commitments.
"If I put on a hat, it does not change what my family is." — Rich Hickey
Maps are collections of keys, not the stuff inside the keys. Extra keys don't change identity:
;; These are the same entity with different knowledge
{:user/id 123 :user/name "Alice"}
{:user/id 123 :user/name "Alice" :user/email "[email protected]"}
;; Identity preserved, knowledge accretedThis is RDF's open-world assumption:
"Spec is about what you CAN do, not about what you CAN'T." — Rich Hickey
Why spec doesn't allow closed maps:
;; If you could say "only these keys"
(s/def ::user (s/closed-keys :req [::id ::name]))
;; Then adding ::email later would BREAK everything
;; Every consumer would need updating
;; Growth becomes breakage
;; Cascading changes up the dependency treeThe open-world discipline:
(select-keys user [::id ::name])Each level is a collection with two operations: add or remove.
"My family doesn't change when I put on a hat." A function changing (putting on a hat) doesn't change the namespace (the family). Don't version the namespace because a function changed. Don't version the artifact because a namespace changed.
Growth happens through accretion:
Breakage happens through:
The discipline: Turn breakage into accretion. Don't remove foo, add foo-2. Don't require new parameters, make them optional. Don't remove keys from returns, add new ones.
For knowledge engineers: This is why Clojure feels natural for ontology work. The language's data model mirrors RDF's assumptions:
Write Clojure code the way you'd design an ontology: Open to extension, explicit about semantics, preserving meaning over time. </core_philosophy>
<repl_driven>
The REPL isn't just a tool—it's how you think about Clojure development. Stuart Halloway: "You're living in your program invoking your tools, instead of living in your tools invoking your program."
Traditional development: Write code → Compile → Run → Debug → Repeat REPL-driven development: Start program → Develop features while running → Test immediately → Never restart
What this means practically:
Standard practice for preserving REPL experiments:
(comment
;; REPL-driven development workflow
(def test-user {:user/id 123 :user/name "Alice"})
(create-user test-user)
(db/query {:user/id 123})
;; These forms don't execute in production
;; But preserve your development process
;; And serve as executable documentation
)Why this matters: The comment block captures your development thinking. Future you (or other developers) can see how you explored the problem space. This is executable documentation of the design process.
I cannot use IDE-integrated REPLs (CIDER, Calva). Terminal-based REPL invocation hangs the bash tool. I work differently:
This is actually good discipline: Code that works without complex REPL state is more robust, testable, and maintainable. </repl_driven>
<data_oriented>
Rich Hickey's guidance: "Just use maps." This isn't laziness—it's profound design insight.
Separate code from data:
Why maps are the default:
;; Good: namespaced keywords
{:user/id 123
:user/name "Alice"
:user/email "[email protected]"
:account/id 456
:account/balance 1000.0
:account/currency :USD}
;; Bad: unqualified keys risk collisions
{:id 123 ; Which id? User or account?
:name "Alice"
:balance 1000.0}Auto-resolved keywords in the namespace:
(ns myapp.users)
;; ::name resolves to :myapp.users/name
{::id 123
::name "Alice"
::email "[email protected]"}Maps should be 95% of your data structures. Records exist for specific cases:
Use records when:
Use maps when:
Don't create records prematurely. Start with maps. Only extract records when you have evidence they're needed. The flexibility of maps compounds over time.
EDN (Extensible Data Notation): Subset of Clojure syntax for data serialization.
Use EDN for:
Use JSON when:
</data_oriented>
<spec>
Critical positioning: Spec provides runtime validation with detailed error messages. It is NOT compile-time type checking. The namespace clojure.spec.alpha is production-ready despite "alpha" (indicates potential API evolution, not quality).
At system boundaries:
Where spec shines:
Don't over-specify:
The judgment call: Spec at boundaries where bad data enters. Don't spec internal implementation details. Trust your code within the boundary.
Register specs with namespaced keywords:
(require '[clojure.spec.alpha :as s])
;; Predicate-based specs
(s/def ::id pos-int?)
(s/def ::email (s/and string? #(re-matches #".+@.+\..+" %)))
(s/def ::role #{:admin :user :guest})
;; Map specs - note: maps are OPEN by default
(s/def ::user
(s/keys :req [::id ::email]
:opt [::role ::created-at]))Co-locate specs with code:
(ns myapp.users
(:require [clojure.spec.alpha :as s]))
(s/def ::id pos-int?)
(s/def ::email string?)
(s/def ::user (s/keys :req [::id ::email]))
(defn create-user
"Creates a new user account."
[user-data]
(let [validated (s/conform ::user user-data)]
(if (s/invalid? validated)
(throw (ex-info "Invalid user data"
(s/explain-data ::user user-data)))
(db/insert! :users validated))))(s/fdef create-user
:args (s/cat :user-data ::user)
:ret ::user
:fn (s/and #(= (-> % :args :user-data ::email)
(-> % :ret ::email))))Instrument in development only:
(require '[clojure.spec.test.alpha :as stest])
;; Development/test: validates :args at call time
(stest/instrument `create-user)
;; Production: turn off (performance overhead)
(stest/unstrument `create-user)Specs automatically generate test data:
(require '[clojure.spec.gen.alpha :as gen])
(gen/sample (s/gen ::user))
;; Generates random valid users
(stest/check `create-user)
;; Property-based testing with generated inputsMaps are open by default: Extra keys allowed. This is intentional—enables evolution. Use s/keys not s/strict-keys unless you have a specific reason.
The spec IS the function: It defines correctness. Design specs carefully.
Don't fight the alpha status: clojure.spec.alpha is stable and widely used. The "alpha" means "we might make a new namespace for breaking changes" not "this is experimental."
spec2/spec-alpha2: Work in progress with no timeline. Stick with spec.alpha for all production code. </spec>
<state_management>
Clojure provides different reference types for different coordination needs. Understanding when to use each is critical.
Atoms (90% of state):
swap!, reset!, compare-and-set!(def app-state (atom {:users {} :sessions {}}))
(swap! app-state update-in [:users user-id] assoc :last-seen (Instant/now))Refs (coordinated transactions):
(def account-a (ref 1000))
(def account-b (ref 500))
(dosync
(alter account-a - 100)
(alter account-b + 100))Agents (asynchronous updates):
send for CPU-bound, send-off for blocking I/O(def background-processor (agent {}))
(send background-processor process-batch data)Vars with dynamic scope:
*out*, *print-length*, context propagationDon't use atoms for everything just because they're simple:
;; Anti-pattern: global mutable state everywhere
(defonce web-server (atom nil))
(defonce db-connection (atom nil))
(defonce cache (atom {}))
(defn start! []
(reset! web-server (create-server))
(reset! db-connection (connect-db)))Better: Component/Mount/Integrant for lifecycle:
(defn create-system []
{:web-server (create-server)
:database (connect-db)
:cache {}})Why: Global atoms create hidden dependencies, make testing hard, and obscure system boundaries. Use lifecycle libraries for managing stateful resources. </state_management>
<common_mistakes>
These patterns are retrieval triggers—seeing them should activate knowledge about the Clojure alternative.
<from_java>
Overusing class hierarchies where maps suffice:
;; Java thinking: types for everything
(defrecord User [id name email])
(defrecord Admin [id name email permissions])
(defrecord Guest [id name])
;; Clojure thinking: just data
{:user/id 123 :user/name "Alice" :user/role :admin :user/permissions #{:read :write}}
{:user/id 456 :user/name "Guest" :user/role :guest}Creating unnecessary mutable state:
;; Java thinking: everything needs state management
(defonce users (atom {}))
(defn create-user! [user-data]
(let [id (generate-id)
user (assoc user-data :user/id id)]
(swap! users assoc id user)
user))
;; Clojure thinking: pass data through
(defn create-user [db user-data]
(let [user (assoc user-data :user/id (generate-id))]
(db/insert! db :users user)))The `contains?` confusion:
;; Java: Collection#contains checks for VALUE
;; Clojure: contains? checks for KEY/INDEX
(contains? {:a 1 :b 2} :a) ; true (key exists)
(contains? {:a 1 :b 2} 1) ; false (1 is not a key)
(contains? [0 1 2] 1) ; true (index 1 exists)
;; For value membership, use:
(some #{1} (vals {:a 1 :b 2})) ; 1 (truthy)Not enabling reflection warnings:
;; Add to dev/user.clj
(set! *warn-on-reflection* true)
;; Causes compiler warnings for reflection
;; Fix with type hints in hot paths onlyMaking everything private:
Clojure trusts developers. Use *.impl.* namespaces for internal details rather than making everything defn-. </from_java>
<from_python>
Expecting eager evaluation:
;; Lazy sequences don't realize until needed
(def results (map expensive-operation data))
;; Nothing has run yet!
;; Force realization when you need it
(doall (map expensive-operation data)) ; Realizes and returns seq
(dorun (map expensive-operation data)) ; Realizes for side effects, returns nil
(into [] (map expensive-operation data)) ; Realizes into vectorPrinting infinite sequences hangs:
;; Don't do this
(println (range)) ; Hangs trying to realize infinity
;; Do this
(println (take 10 (range))) ; Finite!Holding sequence heads causes space leaks:
;; Bad: holds entire sequence in memory
(let [data (range 1000000)]
(println (first data))
(println (last data))) ; Entire sequence retained!
;; Good: don't hold the head
(println (first (range 1000000)))
(println (last (range 1000000))) ; Separate realization</from_python>
<from_javascript>
Trying to use async/await directly:
;; JavaScript async/await doesn't exist in Clojure
;; Use core.async for coordination (not all async operations!)
(require '[clojure.core.async :as async])
(defn fetch-user [id]
(async/go
(let [response (async/<! (http/get (str "/users/" id)))]
(:body response))))Over-using core.async:
;; Don't over-complicate
;; Simple callbacks are fine!
;; Overkill
(async/go
(let [result (async/<! (simple-async-op))]
(process result)))
;; Better: just use a callback
(simple-async-op #(process %))Expecting 'this' binding:
Clojure has no implicit this. Pass data explicitly. </from_javascript>
<from_haskell>
Creating type-level abstractions with macros:
Don't recreate Haskell's type system. Embrace dynamic runtime validation with spec.
Assuming purity enforcement:
;; Clojure: pragmatic impurity
;; Just do side effects where needed
(defn get-user-name []
(println "Name: ")
(read-line))
;; Isolate at boundaries, but don't fight themAvoiding necessary runtime validation:
;; Haskell thinking: types ensure correctness
;; Clojure thinking: validate at boundaries
(defn create-user [user-data]
(let [validated (s/conform ::user user-data)]
(when (s/invalid? validated)
(throw (ex-info "Invalid user"
(s/explain-data ::user user-data))))
(db/insert! :users validated)))</from_haskell>
<from_imperative>
Not using `recur` for tail recursion:
;; Stack overflow: JVM has no TCO
(defn factorial [n]
(if (zero? n)
1
(* n (factorial (dec n)))))
;; Correct: use recur
(defn factorial
([n] (factorial n 1))
([n acc]
(if (zero? n)
acc
(recur (dec n) (* acc n)))))Misusing equality:
;; Use = by default for structural equality
(= 1 1.0) ; false (different types)
(== 1 1.0) ; true (numeric equality only)
;; = for collections
(= [1 2 3] [1 2 3]) ; true</from_imperative> </common_mistakes>
<advanced_topics>
Rich Hickey: "Don't use macros unless you absolutely need to."
Use macros when you need to:
Use functions for everything else. Functions are the default.
Anti-patterns:
;; Don't write a macro for this
(defmacro double [x]
`(* 2 ~x))
;; Write a function
(defn double [x]
(* 2 x))When you do write macros: Create usage examples first. Break complex macros into helper functions. Prefer syntax-quote forms.
Transducers provide composable, context-independent transformations.
Use transducers when:
Don't use when:
;; Without transducers
(->> data
(map parse)
(filter valid?)
(map transform)) ; Three intermediate seqs
;; With transducers (single pass)
(def xf (comp (map parse)
(filter valid?)
(map transform)))
(into [] xf data)
(transduce xf conj [] data)core.async serves specific coordination needs, not all async operations.
Use core.async for:
Avoid core.async for:
thread not go)Key patterns:
go for parking (lightweight, many thousands possible)thread for blocking operations<Always measure before optimizing.
(set! *warn-on-reflection* true) in developmentAnti-patterns:
The 90/10 rule: 90% of time spent in 10% of code. Find that 10% first. </advanced_topics>
<tooling>
clj-kondo must fail builds on errors in CI. Fast, comprehensive static analyzer:
CI configuration:
# Run first in CI (fastest feedback)
clj-kondo --lint src test --fail-level error --parallelLocal `.clj-kondo/config.edn`:
{:linters {:unresolved-symbol {:level :error}
:unused-binding {:level :warning}
:unused-private-var {:level :warning}
:deprecated-var {:level :warning}}
:lint-as {my.macro/defroute clojure.core/def}}Integration:
Already covered extensively above. Key: mandatory for external inputs, optional for internal code.
zprint reformats code from scratch like gofmt or Spotless. Aggressive, opinionated, maximum consistency.
Philosophy: Surrender control of formatting for consistency. Let the tool handle it.
Configuration in `deps.edn`:
:format {:deps {zprint/zprint {:mvn/version "1.2.9"}}
:main-opts ["-m" "zprint.main"]}CI check:
# Check formatting (don't auto-fix in CI)
clojure -M:format "{:search-config? true}" check src test
# Format locally
clojure -M:format "{:search-config? true}" -w src test`.zprintrc` options:
{:style :community
:width 80
:map {:comma? false}}Why zprint over cljfmt: Complete reformatting ensures absolute consistency. cljfmt preserves your line breaks; zprint doesn't trust you to get them right.
"There's a function for that!" Suggests more idiomatic Clojure patterns.
Uses core.logic to find code that could be more idiomatic:
;; Kibit suggests
(if (not x) y z) → (if-not x y z)
(apply concat x) → (mapcat identity x)Use for:
Don't enforce in CI (too opinionated, some suggestions debatable).
Slower but more thorough than clj-kondo. Best for CI where speed isn't critical.
Complements clj-kondo with different checks. Use both if you want comprehensive coverage.
<testing>
For general testing philosophy and TDD principles, see the test-driven-development skill. This section covers Clojure-specific testing practices.
Core testing principle (from test-driven-development skill): Mock at architectural boundaries (external systems, injected dependencies), not internal implementation details.
clojure.test dominates (~85% adoption):
(ns myapp.users-test
(:require [clojure.test :refer [deftest is testing]]
[myapp.users :as users]))
(deftest create-user-test
(testing "creates user with valid data"
(let [user-data {:user/email "[email protected]"}
result (users/create-user user-data)]
(is (pos-int? (:user/id result))
"Generated user ID should be positive")))
(testing "throws on invalid data"
(is (thrown? clojure.lang.ExceptionInfo
(users/create-user {:invalid :data})))))Add test.check for property-based testing:
Leverage spec for generative testing via clojure.spec.test.alpha/check. </testing> </tooling>
<build_tools>
deps.edn has won the build tool debate (70%+ adoption and growing). Leiningen still fine for existing projects.
Why:
Basic structure:
{:deps {org.clojure/clojure {:mvn/version "1.12.3"}}
:paths ["src" "resources"]
:aliases
{:test {:extra-paths ["test"]
:extra-deps {io.github.cognitect-labs/test-runner
{:git/tag "v0.5.1" :git/sha "dfb30dd"}}}
:dev {:extra-deps {org.clojure/tools.namespace {:mvn/version "1.4.4"}}}
:lint {:deps {clj-kondo/clj-kondo {:mvn/version "2025.01.24"}}
:main-opts ["-m" "clj-kondo.main" "--lint" "src" "test"]}
:format {:deps {zprint/zprint {:mvn/version "1.2.9"}}
:main-opts ["-m" "zprint.main"]}}}Compose aliases:
clj -M:dev:test # Development + testing
clj -M:format "{:search-config? true}" -w src testDon't migrate unless you have a reason. Leiningen is stable, batteries-included, and well-supported.
Use Leiningen when:
</build_tools>
<project_structure>
my-project/
├── deps.edn
├── README.md
├── .gitignore
├── .clj-kondo/config.edn
├── .zprintrc
├── src/
│ └── myapp/
│ ├── core.clj
│ ├── users.clj
│ └── db.clj
├── test/
│ └── myapp/
│ ├── core_test.clj
│ └── users_test.clj
├── resources/
│ └── config.edn
├── dev/
│ └── user.clj
└── target/ (gitignored)Large namespaces are acceptable. Clojure core has 500+ symbols in one namespace. Don't over-fragment.
Hard rule: No circular dependencies. If A requires B, B cannot require A.
Naming pattern: [organization].[project].[module]
(ns myapp.users
"User management functionality."
(:require [clojure.spec.alpha :as s]
[clojure.string :as str]
[myapp.db :as db])
(:import [java.time Instant]
[java.util UUID]))Standard library aliases (use these):
[clojure.string :as str][clojure.set :as set][clojure.java.io :as io](ns user
"Development namespace loaded automatically."
(:require [clojure.tools.namespace.repl :refer [refresh]]))
(defn start []
(start-system))
(defn stop []
(stop-system))
(defn reset []
(stop)
(refresh :after 'user/start))
(comment
(start)
(stop)
(reset))</project_structure>
<documentation>
Public API functions require docstrings. Production code expectations.
(defn create-user
"Creates a new user account with the provided data.
Validates `user-data` against the ::user spec and persists
to the database. Returns the created user with generated :user/id.
Arguments:
- `user-data`: Map with :user/email (required), :user/role (optional)
Returns:
- Created user map with :user/id, :user/email, :user/created-at
Throws:
- `ex-info` with spec explanation if validation fails
- `SQLException` if database insert fails"
[user-data]
...)Rules:
[[related-fn]]Metadata:
(defn ^:no-doc internal-helper ...) ; Exclude from docs
(defn ^{:deprecated "1.2.0"} old-fn
"Legacy. Use [[new-fn]] instead."
...)</documentation>
Clojure 1.12.3 (September 2025) is current. Version history: https://github.com/clojure/clojure/blob/master/changes.md
The community upgrades rapidly thanks to exceptional backward compatibility.
Rich Hickey's "Spec-ulation" philosophy:
Practical guidelines:
Clojure 1.12 highlights:
Compatibility: Java 8 minimum, Java 21 LTS recommended, supports through Java 25.
Aggressive adoption applies ONLY to codebases you own.
When contributing to open source:
<related_skills>
</related_skills>
<resources>
Official:
Rich Hickey's Essential Talks (transcripts—machine-readable):
Community:
Tooling:
Books (online):
Performance:
</resources>
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.