It's 5pm on a Friday. CI is green, tests pass, the image is built. You deploy — and the app
crashes on boot because DATABASE_URL isn't set in the environment. It was in the .env on
your machine. It just never made it into the Kubernetes deployment. Nobody notices, because
nothing checks that the three places agree.
This is config drift, and it's one of the most common causes of "it worked on my machine."
Why .env linters don't catch this
Most env-var tooling validates a .env file against a schema you write by hand. That
schema drifts too, and — crucially — it doesn't know what your code actually reads.
If someone deletes a key your app requires, or mistypes DB_HOST as DBHOST, the linter is
happy because the file matches the schema.
The source of truth is your code. So I built a linter that reads it.
What conflint does
conflint parses your source to find every env var /
config key your app actually reads, then reconciles that set against every place the value
should exist — .env, docker-compose, GitHub Actions, Kubernetes and Terraform.
It knows:
| Rule | Meaning |
|---|---|
| CL001 | read in code, defined nowhere |
| CL002 | defined for real, never read by code |
| CL003 | undefined name is one edit away from a known one (typo) |
| CL004 | high-entropy secret committed in a config source |
| CL005 | documented in .env.example but no real source provides it (drift) |
| CL006 | required value defined but empty |
| CL007 | defined twice / conflicting across sources |
| CL008 | secret equals a weak/default placeholder |
| CL009 | used or provided, but undocumented in the example file |
Scanners today: Python (AST), JavaScript/TypeScript, Go, Ruby.
A concrete example
Say your app looks like this:
# app.py
import os
DATABASE_URL = os.environ["DATABASE_URL"]
API_KEY = os.getenv("API_KEY")
REDIS_URL = os.environ.get("REDIS_URL", "redis://localhost:6379")
and your .env looks like this:
API_KEY=sk-live-4f9a2b7c8d1e6f3a5b0c9d8e7f6a5b4c
DATABSE_URL=postgres://localhost:5432/app
Running confl check gives you:
conflint checked 2 files (3 config reads, 2 definitions) in 7ms
.env:1:1
CL004 error: 'API_KEY' looks like a committed secret in dotenv:1
hint: Move real secrets to a secrets manager; keep placeholders in .env.example
app.py:3:16
CL001 error: 'DATABASE_URL' is read in code but not defined in any configuration source:3
hint: Add it to your .env (then run `confl sync` to refresh .env.example)
app.py:5:13
CL001 warning: 'REDIS_URL' is read in code but not defined in any configuration source:5
hint: Add it to your .env (then run `confl sync` to refresh .env.example)
2 files checked - 2 errors, 3 warnings, 0 infos
Three real problems caught in 7 milliseconds: a committed secret, a missing required variable,
and (implicitly) a typo — note DATABSE_URL is defined but never read, which is why
DATABASE_URL is "missing."
Quickstart
pip install conflint
confl check # lint the whole project
confl sync # regenerate an accurate .env.example
confl explain DATABASE_URL # who reads it? where is it defined?
Wire it into CI and pre-commit
GitHub Action (inline PR annotations):
- name: Lint configuration
uses: conflint/conflint@v0.1.2
with:
reporter: github
fail-level: error
Or a pre-commit hook:
- repo: https://github.com/conflint/conflint
rev: v0.1.2
hooks:
- id: conflint
Output formats: text, --json (stable schema), --sarif (GitHub/GitLab/Azure code scanning),
--github (workflow annotations), and JUnit XML for Jenkins.
Honest limitations
- It's early (v0.1.2, MIT). The rule set and source coverage are opinionated and evolving.
- Static analysis means it infers reads; dynamically constructed keys (e.g.
os.environ[prefix + x]) can't be resolved and may needignore-names. - It's not a secret scanner replacement — it catches secrets specifically in config sources, not across full git history (use gitleaks/trufflehog for that).
Try it / feedback
- Repo: https://github.com/conflint/conflint
- Docs: https://conflint.readthedocs.io
- PyPI: https://pypi.org/project/conflint
I'd love feedback on the rule set and which scanners/sources to add next. If it's useful,
a ⭐ helps others find it.

Top comments (0)