Keep .env and .env.example in sync, automatically, through Git.
Status: early (v0.1). The core is small and tested, but it hasn't seen heavy real-world use yet. Issues and feedback are very welcome.
- What it is: a small command-line tool (single Go binary, no runtime, standard library only) that installs Git hooks to keep
.env.exampleand.envfrom drifting apart. - What it does: before you commit or push, offers to add new variable names to
.env.example; after you pull or switch branches, offers to add new variables to your.env. - What it never does: read or copy
.envvalues, use the network (except an explicitgit-envdiff update), collect telemetry, or overwrite your existing Git hooks. - Works with: any language or framework (it only looks at two files), macOS, Linux, and Windows (Windows is newer and less battle-tested).
- Install:
curl -fsSL https://github.com/tejjasdev/git-envdiff/releases/latest/download/install.sh | sh, thengit-envdiff install. - License: MIT.
You add a new environment variable to your local .env file. It works, you
commit, you push. Three days later a teammate pulls your change, runs the
app, and it crashes — because .env.example never got the new variable, and
their .env doesn't have it either. Nobody notices until it breaks
something.
.env.example drifts out of sync with .env constantly, because updating it
by hand is a step everyone forgets under normal working conditions. It's not
a hard problem — it just needs something watching for it.
Install it once. From then on, it works silently in every Git repository on your machine:
- Before you commit or push — if
.envhas a variable that.env.exampledoesn't, it asks whether to add it (name only, never the value) and folds the fix into your commit. - After you pull or switch branches — if
.env.examplenow has a variable your.envis missing, it asks whether to add it (with the template's own — already public — default value). - When there's nothing to say, it says nothing. No output, no delay, no daily nag.
$ git commit -m "add Stripe integration"
git-envdiff: .env defines STRIPE_KEY, not in .env.example.
Add to .env.example (name only, no value)? [y/N/l]: y
git-envdiff: added STRIPE_KEY to .env.example and included it in this commit.
$ git pull
git-envdiff: .env.example has variable(s) missing from your .env: REDIS_URL
Add to .env (values copied from .env.example, usually blank)? [y/N]: y
git-envdiff: added REDIS_URL to .env. Fill in real values for any that are blank.
It works the same way in a Python repo, a Node repo, a Go repo, or anything
else — it only looks at .env and .env.example, never at your language or
framework.
git-envdiff does one job. On purpose, it is not:
- a secrets manager, vault, or encryption tool
- a cloud service, dashboard, or account of any kind
- a
.envloader or a replacement fordotenv - a schema/validation framework
- a CI platform or IDE plugin
- collecting telemetry of any kind
If you're looking for a richer environment-configuration system with schema validation and type checking, tools like dotenvx or varlock solve a bigger problem than this one. See docs/COMPARISON.md for how git-envdiff relates to those and to other tools in this space.
macOS / Linux:
curl -fsSL https://github.com/tejjasdev/git-envdiff/releases/latest/download/install.sh | shWindows (PowerShell):
irm https://github.com/tejjasdev/git-envdiff/releases/latest/download/install.ps1 | iexPrefer to read the script first, or install by hand? See
install.sh / install.ps1, or grab a binary
directly from the Releases page
and verify it against that release's checksums.txt.
Go toolchain:
go install github.com/tejjasdev/git-envdiff@latestWindows is newer and less battle-tested than macOS/Linux: the installer and prompts have been exercised under PowerShell 7 and cross-compiled, but not yet used on a large number of real Windows machines. Reports are welcome.
Then, once per machine:
git-envdiff installOn Git 2.54+ this registers git-envdiff globally — every repository on your
machine, from that point on, without touching that repository again. On
older Git it installs into the current repository only (git-envdiff install again in each one, or upgrade Git). Either way, your existing hooks
— Husky included — are never overwritten.
| Command | What it does |
|---|---|
git-envdiff |
Check both directions right now and offer to fix |
git-envdiff install [--repo] |
Install hooks (global by default on Git ≥ 2.54; --repo forces this repository only) |
git-envdiff uninstall [--repo] |
Remove exactly what install added |
git-envdiff status |
Show what's currently installed |
git-envdiff on / off |
Enable / disable git-envdiff in the current repository |
git-envdiff update |
Update git-envdiff itself |
git-envdiff --version |
Print the version |
Temporary bypass for one command: GIT_ENVDIFF_SKIP=1 git commit ... (this
also works with Git's own --no-verify, which skips every hook).
Git has no single "just ran a pull" hook, so git-envdiff listens on four:
pre-commit and pre-push (checking .env against .env.example), and
post-merge and post-checkout (checking the other direction, after a pull
or branch switch). Each one is a no-op in under a few milliseconds when
there's nothing to report.
The full reasoning — why these four hooks and not others, how the two install strategies work, and what was verified against real Git rather than assumed — is in docs/HOW_IT_WORKS.md.
This is the part that matters most for a tool that reads your .env file
and runs automatically during Git commands:
- git-envdiff reads variable names only. It never reads, logs,
prints, or stores a value from your
.envfile. This is enforced by the file-handling code itself (seeinternal/envfile) — there is no code path that copies a.envvalue anywhere. - It only ever appends a bare
KEY=line to.env.example. Never a value, never a reorder, never a rewrite of existing lines. - No network access, ever, except
git-envdiff update— and even that only runs when you explicitly type it. No telemetry, no phone-home, no "checking for updates" on startup. - No account, no cloud, no dashboard. Nothing about your project leaves your machine.
- If the git-envdiff binary is ever missing (uninstalled, or a PATH a
GUI Git client doesn't share with your shell), every hook silently does
nothing rather than breaking your commits — verified in
internal/install's test suite. - A crash inside git-envdiff never fails your Git command. Every hook
exits
0except in one specific case: accepting a fix atpre-pushtime intentionally stops the push, because the fix can't retroactively join commits that are already being pushed — see docs/HOW_IT_WORKS.md for why.
If you find a case where any of this isn't true, please open an issue — this list is a set of guarantees, not just intentions.
| Two-way drift check | Automatic via Git hooks | One install for every repo | Runtime needed | |
|---|---|---|---|---|
| git-envdiff | ✅ | ✅ | ✅ (Git ≥ 2.54) | none |
| dot_example | ✅ | ✅ (per project) | ❌ | Ruby |
| clean-dotenv | one way (regenerates .env.example) |
via pre-commit framework | ❌ | Python |
| dotenvx | one way (ext genexample, on demand) |
opt-in precommit guard only | n/a | none / Node |
| varlock | replaces the pair with a schema | leak scanning | n/a | none / Node 22+ |
| dotenv-linter | compare on demand | ❌ | n/a | none |
git-envdiff isn't trying to out-feature these tools — several of them do
things it deliberately never will. The gap it fills is narrow: keep the
plain .env / .env.example convention, get automatic checks in both
directions through Git itself, and install it once. Details, and where each
of the others is the better choice, are in
docs/COMPARISON.md (checked against each project's own
documentation in September 2026).
New to the .env / .env.example pattern, or want the manual approach?
See the guide.
Will this ever write a secret value into a file I commit?
No — see Privacy and security above. AppendBlankKeys,
the only function that writes to .env.example, has no parameter for a
value at all.
I already use Husky / lefthook / another hook manager. Will this break it?
No. git-envdiff detects core.hooksPath and prints the one line to add to
your existing hook files instead of writing its own — it never overwrites a
hook manager's setup. See docs/HOW_IT_WORKS.md.
Does this work with [my language/framework]?
Yes — it only reads .env and .env.example; it has no idea what language
your project is in, on purpose.
What if I don't want to be asked about a particular variable?
Answer l (local-only) at the prompt, or run GIT_ENVDIFF_SKIP=1 for a
single command. Both are documented above.
Does this replace dotenv, dotenvx, or a .env loader?
No — git-envdiff never loads environment variables into a process. It only
compares two files. Keep using whatever loader your project already uses.
git-envdiff uninstall # removes whatever `install` added
rm ~/.local/bin/git-envdiff # or wherever you installed the binary
rm -rf ~/.git-envdiffBug reports, ideas, and pull requests are welcome — see
CONTRIBUTING.md. The whole codebase is Go standard
library only, organized as small, independently-testable packages under
internal/; go test ./... is the whole test suite.