A static analysis tool for Clojure that validates code using Guardrails specs at development time. It uses generators to flow sample data through your code and catch type errors without running it.
-
Static Type Checking: Validates function arguments and return values against Guardrails specs without running code
-
Path-Based Analysis: Tracks execution paths through conditional branches for precise error reporting
-
IDE Integration: Language Server Protocol (LSP) support for real-time feedback in your editor
-
Sample-Based Validation: Uses spec generators to create test data and validate type correctness
-
Zero Runtime Overhead: All analysis happens at development time
-
Comprehensive Coverage: Supports control flow (
if,when,cond), higher-order functions, macros, and more
In your deps.edn, add the analyzer as a dev dependency and set the JVM mode:
{:deps {com.fulcrologic/guardrails {:mvn/version "1.2.9"}
;; ... your other deps
}
:aliases {:dev {:extra-deps {com.fulcrologic/guardrails-analyzer {:mvn/version "CURRENT"}}
:jvm-opts ["-Dguardrails.mode=:pro"]}}}The -Dguardrails.mode=:pro property is essential — it tells the >defn macro to register spec
information that the analyzer needs. Without it, the analyzer has nothing to work with.
Start your REPL with the dev alias, then load your application:
clojure -A:dev -M:nrepl # or however you start your REPL;; Load your app so specs are registered
(require 'my.app.main)(require '[com.fulcrologic.guardrails-analyzer.checkers.repl :as ga])
(ga/start)The daemon (a background process that bridges the analyzer to your editor) auto-launches if one isn’t already running. You’ll see a log message confirming the connection.
From your editor: Use editor commands like IntelliJ’s "Check Namespace" (under Tools > Guardrails Analyzer).
From the REPL:
;; Returns problems as data (works without any editor)
(ga/check-ns 'my.app.core)
;; => [{:file "..." :line 42 :column 3 :severity "error" :message "..."}]
;; Pushes results to your editor as diagnostic annotations
(ga/check-ns! 'my.app.core)
;; Both also work with file paths
(ga/check-file "src/main/my/app/core.clj")
(ga/check-file! "src/main/my/app/core.clj")The check-ns / check-file variants return a compact vector of problems — useful from a pure
REPL with no IDE, for scripting, or for building lightweight editor integrations (Vim, Emacs, etc.).
The ! variants push results through the daemon to your connected editor.
The analyzer works by generating sample data from your specs and flowing it through your code. Understanding this is key to getting good results.
(>defn calculate-discount
[price :- number?
customer-type :- keyword?]
[number? keyword? => number?]
(if (= customer-type :premium)
(* price 0.9)
price))The analyzer:
-
Parses the gspec:
[number? keyword? ⇒ number?] -
Generates sample values (e.g.
42,:foo) from the specs -
Traces execution through both branches of the
if -
Validates that all paths return a
number?
If you accidentally return a non-number:
The Return spec is number?, but it is possible to return
a value like :invalid when (= customer-type :premium) -> elseWithout ^:pure, the analyzer treats function calls as black boxes — it knows the return spec
but can’t see how data flows through the function. This leads to false positives.
Consider these two functions:
(>defn with-full-name [{:person/keys [first-name last-name] :as person}]
[(s/keys :req [:person/first-name :person/last-name])
=> (s/keys :req [:person/full-name])]
(assoc person :person/full-name (str first-name " " last-name)))
(>defn greeting-for [person]
[(s/keys :req [:person/first-name :person/last-name])
=> string?]
(let [p (with-full-name person)]
(str "Hello, " (:person/full-name p))))Without ^:pure: The analyzer knows with-full-name returns something matching
(s/keys :req [:person/full-name]), but it generates new random samples for the return value.
The input data (:person/first-name, :person/last-name) is lost. If greeting-for tried to
access :person/first-name from p, the analyzer would warn that it might not be there — even though with-full-name passes the whole map through via assoc.
With ^:pure: The analyzer actually runs the function on the sample data, so p contains
the real result — including all the keys that flowed through:
(>defn with-full-name [{:person/keys [first-name last-name] :as person}]
^:pure [(s/keys :req [:person/first-name :person/last-name])
=> (s/keys :req [:person/full-name])]
(assoc person :person/full-name (str first-name " " last-name)))Now the analyzer can trace data flow accurately through with-full-name and won’t issue
false warnings about missing keys downstream.
Rule of thumb: Mark any function as ^:pure if it:
-
Has no side effects (no I/O, no mutation, no state changes)
-
Transforms data in a way that downstream code depends on
This is the single most impactful thing you can do to improve analyzer accuracy.
If a function has side effects but you still want the analyzer to understand its data flow, provide a pure stand-in:
(>defn save-and-return! [conn person]
^{:pure-fn (fn [_ person] (assoc person :person/saved? true))}
[any? (s/keys :req [:person/name]) => (s/keys :req [:person/name :person/saved?])]
(db/save! conn person)
(assoc person :person/saved? true))The :pure-fn is never called at runtime — it’s only used by the analyzer to simulate data flow.
(ga/start)The analyzer checks against currently-loaded code. You manage your own code loading and reloading. This is the best option for most users — the analyzer never interferes with your running system.
(ga/start {:src-dirs ["src/main" "src/dev"]})Auto-reloads changed namespaces when the editor triggers a refresh-and-check. Use a separate REPL that you don’t work in directly. This is useful if you want fully automatic checking without manual reloads.
The daemon is a shared background process that bridges the analyzer and your editor.
It auto-launches when you call start. One daemon serves all projects on your machine.
To start manually:
clojure -Sdeps '{:deps {com.fulcrologic/guardrails-analyzer-daemon {:mvn/version "1.0.0-alpha2"}}}' \
-M -m com.fulcrologic.guardrails-analyzer.daemon.main-
Port file:
~/.guardrails/daemon.port -
Logs:
~/.guardrails/
-
IntelliJ: Plugin at https://github.com/fulcrologic/guardrails-intellij-plugin. The plugin auto-discovers the daemon and provides check commands under Tools > Guardrails Analyzer.
-
Other editors: The daemon runs an LSP server. Configure your editor’s LSP client to connect to the port advertised in
~/.guardrails/lsp-server.port.
This project has a close relationship with the Guardrails library:
-
Guardrails provides the
>defnmacro and inline gspec syntax -
Guardrails Analyzer performs static analysis on code using those specs
-
Changes may require coordinated updates in both repositories
-
Core library function specs are defined in
analysis/fdefs/
See User Guide for detailed usage documentation, including spec writing guidelines and editor-specific setup.