v0.2.1 · macOS & Linux · MIT

Your .env, typed.

.env.example is untyped text. dottyenv replaces it with a schema that says which variables are required, what they should look like, and where to go get them. A missing key fails at startup with a link, not twenty minutes in with a stack trace.

$ brew tap mfung/tap && brew install dottyenv

// 01 · the schema

Commit the shape of your config, not its values.

Point dottyenv init at the .env.example you already have. It fills in patterns and source URLs for providers it recognises, and leaves everything else as a TODO. A wrong pattern is worse than no pattern, so it never guesses.

.env.examplebefore
DATABASE_URL=postgres://localhost/app
STRIPE_SECRET_KEY=sk_test_xxx
GITHUB_TOKEN=
LOG_LEVEL=info
PORT=3000
→dottyenv init
dottyenv.tomlafter
[vars.DATABASE_URL]
required    = true
pattern     = '^postgres(ql)?://'
description = 'PostgreSQL connection string'
secret      = true

[vars.STRIPE_SECRET_KEY]
required    = true
pattern     = '^(sk|rk)_(test|live)_'
source      = 'https://dashboard.stripe.com/apikeys'
secret      = true

[vars.LOG_LEVEL]
required    = true
# one_of = ["info", ...]  # TODO: list the valid values

// 02 · four commands

Each one fixes a way .env files break.

None of them create values. Providers do that. dottyenv handles everything after the value exists.

init

onboarding

Infer a schema from an existing .env or .env.example. New teammates learn what each variable is and where to get it.

wrote dottyenv.toml (6 variables from .env.example)
review it, then run `dottyenv check`

check

late failure

Validate .env against the schema. Exits non-zero on failure, so it doubles as a CI gate.

MISSING   STRIPE_SECRET_KEY
          → https://dashboard.stripe.com/apikeys
$ echo $?
1

scan

drift

Cross-reference the schema against your code. Works in any language, Dockerfiles and CI config included, because it keys off the substring env.

used but not declared
  SENDGRID_API_KEY       src/server.ts:2
  SESSION_SECRET         app.rb:1

declared but never mentioned
  LEGACY_UNUSED_FLAG

list

at a glance

Every declared variable, whether it's set, and whether it's required. Add --json for scripts.

DATABASE_URL         set      required
GITHUB_TOKEN         set      required
LOG_LEVEL            set      required
STRIPE_SECRET_KEY    MISSING  required

// 03 · the catalog

Every tool can say a key is missing. This one says where to get it.

Vendors give their secrets recognisable prefixes. dottyenv ships a small catalog of them, so init writes the pattern and the source URL for you. Patterns match the prefix only, never the length: a key that changes length next year still passes.

PostgreSQL secret

postgres://postgresql://

MySQL / MariaDB secret

mysql://mysql2://mariadb://

MongoDB secret

mongodb://mongodb+srv://

SQLite not secret

sqlite://sqlite3://file:

Redis secret

redis://rediss://

Stripe secret key secret

sk_test_sk_live_rk_test_rk_live_

Stripe webhook secret secret

whsec_

Stripe publishable key not secret

pk_test_pk_live_

GitHub token secret

ghp_gho_ghu_ghs_ghr_github_pat_

AWS access key ID secret

AKIAASIA
→ IAM security credentials

AWS secret access key secret

matched by name, no pattern
→ IAM security credentials

AWS session token secret

matched by name, no pattern
Embedded in the binary. Zero network calls. Stripe, GitHub and AWS formats checked against vendor docs. Your hand-written schema always wins.

// 04 · environments

SQLite locally, Postgres in production.

One variable, two shapes. Declare it once and override per environment. The overlay is picked from the filename, so .env.production gets production rules. A misspelled --env is an error, never a silent downgrade of your production gate.

  • overriderequired, pattern, one_of, default: the constraints.
  • fixeddescription, source, secret: the identity. A credential isn't sensitive in prod and safe in dev.
dottyenv.toml
[vars.DATABASE_URL]
required    = true
description = "Primary database"
secret      = true

[envs.development.vars.DATABASE_URL]
pattern = '^(sqlite:|file:)'

[envs.production.vars.DATABASE_URL]
pattern = '^postgres(ql)?://'

[envs.production.vars.STRIPE_SECRET_KEY]
required = true
pattern  = '^sk_live_'
01

Loose beats precise

One false ✗ and people stop trusting the output. Patterns are never inferred from a single sample.

02

Secrets stay secret

Values marked secret are reported by length only. An error message is the last place a password should surface.

03

Local-first

No account, no server, no network calls. One TOML file in your repo and one binary on your path.

04

Built for pipes

Data on stdout, diagnostics on stderr, --json everywhere, and exit codes you can branch on in CI.

// 05 · what it is not

It validates. That's all.

dottyenv doesn't store or ship secrets, so it sits next to whatever you already use for that.

ToolWhat it doesHow dottyenv differs
dotenvxEncrypts .env for gitDoesn't encrypt; it validates
direnvAuto-loads env on cdExplicit, and runs in CI too
SOPSEncrypts structured filesA different problem entirely
Doppler / InfisicalHosted secret syncLocal-first, no account
dotenv-linterLints .env syntaxChecks meaning against a schema

// install

One binary. No Rust toolchain.

Prebuilt for macOS and Linux on x86_64 and ARM. Linux builds target a glibc 2.28 floor, so they run on RHEL 8, Ubuntu 20.04 and newer.

Every release ships a SHA256SUMS. See Releases for tarballs and the current version.

$ brew tap mfung/tap
$ brew install dottyenv

Works on macOS and Linux.