goenv: a .env editor that reads, checks, writes and converts env files
A Go CLI plus an `env` package for inspecting, editing and re-formatting `.env` files without overwriting the shell.
The repository andreimerlescu/goenv ships two related tools under one module: a standalone binary named goenv, and an importable Go package at github.com/andreimerlescu/goenv/env. Both are Apache-2.0 licensed, written for Go 1.24.5, and small — about 100 KB across 30 files.
The author's stated motivation in the README is plain: "sometimes I just need to work with an env file, even if it doesn't yet exist." The tool leans into that, offering validation of .env syntax and transformation of a .env file into several other formats. It does not try to be a secrets manager or a full process supervisor; it reads, checks, edits and re-renders environment files.
Two halves: a CLI and a library
The command-line entry point is thin. The CLI parses its arguments through a figtree plant — Run(figs figtree.Plant) using github.com/andreimerlescu/figtree/v2. Most flags are copied into a stateful struct; version, verbose, and clean-all handling remain directly in run.go.
The second half is the env package, which can be used on its own:
go get -u github.com/andreimerlescu/goenv/env
When imported, it automatically loads ./.env (if present) into the runtime environment without overwriting variables that are already set. You can point it at a different file with AM_GO_ENV_FILE, or disable auto-loading entirely with AM_GO_ENV_NEVER_LOAD_FILE=true.
The package offers typed access to environment values — including a Bool accessor — along with file-loading controls set through the surrounding environment, such as AM_GO_ENV_FILE and AM_GO_ENV_NEVER_LOAD_FILE=true.
The command line, flag by flag
The CLI is built around short verb flags rather than subcommands. From the README's usage example:
go install github.com/andreimerlescu/goenv@latest
goenv -init -write -file n8n.env
goenv -write -file n8n.env -add -env DATA_FOLDER -value "$(pwd)"
goenv -file n8n.env -print
goenv -file n8n.env -xml
goenv -file n8n.env -has -env GENERIC_TIMEZONE
The flag strings map onto constants the parser reads. The core verbs:
-add— insert a key using-envand-value;-rmremoves lines whose key (or value) matches-env/-value-hastests whether a named key exists;-isexits 0 if any line's value equals-value-not— negate-hasor-is-write— persist changes back to the file (defaults off)-init— create the target file if it does not exist-print— print the resulting contents before exiting-json,-yaml,-toml,-ini,-xml— render the env map in another format, written to<file>.<ext>when combined with-write-mkallemits all five formats at once;-cleanallremoves generated format files when combined with-writeor-rm, unlessAM_GO_ENV_NEVER_DELETE=true- a production flag (
argProd) and a verbose flag, plus-vfor version
The parsing logic in run.go reads the file line by line, trims whitespace, skips lines shorter than three characters, splits each on a separator (env.MapItemSeparator, split into two parts via MapSplitN) and builds a map[string]string. The -rm flag drops matching keys during this pass; -add inserts only if the key is not already present. The exit code doubles as an assertion for the positive case: -has exits 0 when the key is found. To test absence in a non-empty file you use -not -has, since a plain -has on a missing key there falls through to normal completion rather than failing. A zero-byte file exits 1 for read-only operations.
Production protection
A notable behavior is how goenv treats .env.production. A path containing .env.production, or the production flag (argProd), sets isProd, and state.prodProtected becomes true unless the environment variable named by EnvNeverWriteProduction says otherwise. (An arbitrary path that merely contains "prod" is later overridden by this check.) When writing to a protected production file, result.go halts with:
HALT: PRODUCTION IS PROTECTED! WRITE OPERATION CANCELED.
and exits 1 before touching disk. The production guard halts writes to a protected env file. The production guard in result.go halts writes to protected files; other exit-1 refusals exist for missing files and combined output formats.
Sanity() in sanity.go enforces another rule: only one output format per invocation. Combining -json -ini -toml -yaml triggers an error and exit 1; the tool enforces a single output format per invocation (unless -mkall is used).
Building and testing
The Makefile cross-compiles with CGO disabled for five targets — darwin amd64/arm64, linux amd64/arm64, windows amd64 — stripping symbols and DWARF info via -ldflags "-s -w". Every build target depends on test, which runs ./test.sh.
Testing is shell-driven via ./test.sh: the Makefile's test target runs it, and every build target depends on test. The README's log shows 21 numbered checks covering much of the CLI surface: asserting keys exist or don't (-has, -not -has), checking values (-is), adding and removing entries, converting to several formats, creating a new file with -write, printing versions, and confirming that an add against a missing file fails without -write. The run ends with "All 21 tests PASS!"
The repository includes a top-level VERSION file and a version.go, and goenv -v prints a version string (the README log shows v0.0.2).
Who it is for
This is a developer utility, not an end-user product. The CLI suits scripting and CI-style assertions over configuration files — checking that a required key exists before a deploy, seeding a fresh .env, or converting one into the format another tool expects. The env package serves Go applications that want typed environment access with a .env file loaded automatically but never overriding real environment variables. If you already manage your own env loading and need nothing else, goenv may be redundant; if you work across formats or want shell-friendly assertions on env files, it fills a narrow gap.
andreimerlescu/goenv: 100 KB in 30 files, indexed Nov 9, 2026 after 0.00 hours of indexing. License: Apache-2.0. Source: https://github.com/andreimerlescu/goenv.
Ask your agent to name it in its tool calls (the repo argument of query_graph, recall_project and read_project_source).
Written by AI models on the operator's own hardware, checked by a second model, from the repository's README and its code. Back to the shelf