env-sync-in-go
env sync in go
No description provided on GitHub.
env-sync
Detect and fix drift between
.envand.env.examplefiles ā
The FAFO
I build this stuff jst for fun, i'm bored by doing Machine learning(feature eng, optimization ) stuff, so i tried this and lock in for 3-4 hrs, doing FAFO with docs and blogs and built in one gooo....
Demo
$ env-sync check
Comparing .env ā .env.example
MISSING IN .env.example (3 keys ā your teammates won't have these):
ā STRIPE_WEBHOOK_SECRET
ā ENABLE_BETA
ā MAX_CONNECTIONS
MISSING IN .env (2 keys ā you might be missing required config):
ā LEGACY_API_URL
ā OLD_SECRET
EMPTY VALUES in .env.example (2 keys):
ā DATABASE_URL
ā JWT_SECRET
Matched: 14 keys ā
Run `env-sync fix` to auto-update .env.example
Installation
go install github.com/satyammistari/env-sync@latest
Or build from source:
git clone https://github.com/satyammistari/env-sync
cd env-sync
go build -o env-sync.exe .
Usage
Check for drift
# Default: compares .env ā .env.example
env-sync check
# Custom paths
env-sync check --source .env.production --target .env.staging
# CI mode ā exits code 1 on any drift, plain output, no color
env-sync check --ci
# Strict mode ā also flag empty values in .env as errors
env-sync check --strict
# Ignore specific keys
env-sync check --ignore "LEGACY_KEY,DEPRECATED_SECRET"
Auto-fix your .env.example
# Preview what would change (always run this first)
env-sync fix --dry-run
# Apply the fix ā adds missing keys with smart placeholders
env-sync fix
# Fix without creating a backup file
env-sync fix --backup=false
Example dry-run output:
Would add 3 keys to .env.example:
+ STRIPE_WEBHOOK_SECRET=your-api-secret-here
+ ENABLE_BETA=false
+ MAX_CONNECTIONS=100
Run without --dry-run to apply changes.
Visual diff
# Git-style diff in the terminal
env-sync diff
# Machine-readable JSON output (great for scripts)
env-sync diff --format json
# ASCII table format
env-sync diff --format table
CI/CD Integration
Add this step to your GitHub Actions workflow to catch drift before it ships:
- name: Check env drift
run: env-sync check --ci
This exits with code 1 if any keys are missing from .env.example, blocking the merge automatically.
Why Go over Rust?
env-sync is a pure file I/O tool ā it reads two small text files and compares their keys. There is no CPU-intensive computation, no parallel processing of gigabytes of data, no need for zero-cost abstractions. The performance bottleneck simply doesn't exist here.
Go was the right choice for three concrete reasons: it compiles to a single self-contained binary that anyone can go install in one command, its cobra ecosystem is the gold standard for CLI structure (used by kubectl and the GitHub CLI), and its standard library handles file I/O and string processing cleanly without reaching for external crates. I use Rust where it actually matters ā log-anonymizer uses rayon for parallel processing of multi-GB log files where the performance difference is measurable and real. Choosing the right tool for the right job is the trade-off.
Architecture
env-sync/
āāā cmd/ # CLI commands (check, fix, diff, root)
āāā internal/
ā āāā parser/ # .env file parsing ā isolated, tested independently
ā āāā differ/ # Diff engine ā pure logic, no I/O, no output
ā āāā fixer/ # Placeholder suggestion engine + file writing
ā āāā reporter/ # All terminal output ā colors, formatting, CI mode
āāā testdata/ # Sample .env files for integration tests
āāā main.go
Each concern lives in its own package under internal/ ā meaning nothing outside this module can import them. The parser knows nothing about output formatting. The differ knows nothing about how files are read. The reporter knows nothing about business logic. This separation means every package can be tested in complete isolation, and changing the output format never touches the parsing logic.
Engineering Trade-offs
1. Regex-free parser over regex for correctness
Parsing .env files with regex breaks on edge cases: quoted values with spaces, inline comments, backslash-escaped characters, and the export KEY=value prefix pattern. I chose a character-by-character state machine parser instead. It's ~40 more lines of code but handles every real-world .env format correctly. Correctness over brevity.
2. Alphabetically sorted diff output over file-order output When you have 30 missing keys, seeing them in random insertion order is harder to scan than alphabetical order. The differ sorts all output slices before returning them. This is a small product decision that meaningfully improves readability at the cost of a trivial sort operation.
3. Never copy real values ā only suggest placeholders
The fix command could copy actual values from .env into .env.example to make onboarding faster. I deliberately chose not to do this. .env.example files get committed to git. Copying real values ā even accidentally ā is a secret leak. The suggestion engine generates safe placeholders based on key name patterns instead.
Flags Reference
| Flag | Command | Default | Description |
|---|---|---|---|
--source | all | .env | Path to source file |
--target | all | .env.example | Path to target file |
--no-color | all | false | Disable colored output |
--ci | check | false | CI mode, exit 1 on drift |
--strict | check | false | Treat empty values as errors |
--ignore | check | "" | Comma-separated keys to ignore |
--dry-run | fix | false | Preview without writing |
--suggest | fix | true | Smart placeholder suggestions |
--backup | fix | true | Backup before modifying |
--format | diff | pretty | Output format: pretty/json/table |
Exit Codes
| Code | Meaning |
|---|---|
0 | No drift ā all keys in sync |
1 | Drift found ā keys missing or empty |
2 | Error ā file not found, parse failure |
Contributing
Pull requests are welcome. For major changes, open an issue first to discuss what you'd like to change. Make sure all tests pass before submitting:
go test ./... -v -race