Documentation
dottyenv validates .env files against a schema, dottyenv.toml, that you commit
to the repo in place of .env.example. It holds no secrets, only their shape.
Getting started
Point init at an existing .env or .env.example. It writes a schema and fills in patterns and source URLs for providers it recognises.
$ dottyenv init # writes dottyenv.toml $ dottyenv check # validate .env against it
Review the generated file before committing it. Anything init couldn't infer is left as a # TODO comment rather than guessed at. Every variable present in the source file is marked required, so loosen anything that is actually optional. init won't overwrite an existing schema unless you pass --force.
Schema
One file, one table per variable. The field set is small on purpose.
[meta] version = 1 [vars.STRIPE_SECRET_KEY] required = true pattern = "^sk_(test|live)_" source = "https://dashboard.stripe.com/apikeys" description = "Server-side Stripe key. Use sk_test_ locally." secret = true [vars.LOG_LEVEL] required = false default = "info" one_of = ["debug", "info", "warn", "error"]
| Field | Meaning |
|---|---|
required | The variable must be present in the env file. |
pattern | A regex the value must match. Match the prefix; don't end-anchor on length. |
one_of | The value must be one of these strings. |
default | The value used when the variable is absent. |
source | Where a human goes to get the value. Printed when the variable is missing. |
description | What the variable is for. |
secret | Never print the value. Problems are reported by length only. |
Loose beats precise
A false rejection does more damage than a missed check: one wrong โ and people stop trusting the output. Write patterns that survive a vendor changing key length.
pattern = "^sk_(test|live)_" # good: survives length changes pattern = "^sk_(test|live)_[A-Za-z0-9]{24}$" # bad: breaks when Stripe changes length
Commands
dottyenv init
Generate dottyenv.toml from an existing env file. Matches values by prefix against the built-in provider catalog, falling back to the variable name when the value is a placeholder.
dottyenv check
Validate the env file against the schema. This is the core command and the one to run in CI.
$ dottyenv check โ 3 problems in .env INVALID DATABASE_URL expected ^postgres(ql)?://, got "<redacted, 47 chars>" PostgreSQL connection string INVALID LOG_LEVEL expected one of [debug, info, warn, error], got "verbose" MISSING STRIPE_SECRET_KEY Stripe secret key. Use sk_test_ locally. โ https://dashboard.stripe.com/apikeys 4 of 6 required variables OK
dottyenv scan
Cross-reference the schema against the code. It answers two questions:
- Declared but never mentioned. A literal search for each declared name, so it works anywhere: any language, Dockerfiles, Helm charts, CI config.
- Used but not declared. A single pattern keyed off the substring
env, which every language's syntax contains (process.env.FOO,os.Getenv("FOO"),ENV["FOO"], and so on).
$ dottyenv scan used but not declared SENDGRID_API_KEY src/server.ts:2 SESSION_SECRET app.rb:1 declared but never mentioned LEGACY_UNUSED_FLAG note: 1 file accesses the environment dynamically, so this list may be incomplete: src/server.ts
scan always exits 0. It is advisory; check is the gate.
dottyenv list
Print each declared variable, whether it is set, and whether it is required.
Global flags
| Flag | Effect |
|---|---|
-f, --file <PATH> | Env file to read. Default .env. |
-s, --schema <PATH> | Schema to validate against. Default dottyenv.toml. |
-e, --env <NAME> | Environment overlay to apply. Default: inferred from --file. |
--json | Machine-readable JSON on stdout. |
-q, --quiet | Suppress non-error output. |
Environments
SQLite locally and Postgres in production is one variable with two shapes. Declare it once and override per environment:
[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)?://'
The environment comes from the filename, so .env.production uses the production overlay. --env overrides it:
$ dottyenv check --file .env.production # infers production $ dottyenv check --file .env.staging --env production # staging against prod rules
Overlays may override required, pattern, one_of and default. They may not override description, source or secret, and trying to is a load error. A variable means the same thing everywhere.
--env is an error rather than a silent fallback, since a fallback would quietly downgrade a production gate. An unrecognised filename does fall back quietly, because .env.local is a common name that isn't an environment.Exit codes
| Code | Meaning |
|---|---|
0 | All required variables present and valid |
1 | Validation failure: missing or malformed variables |
2 | Usage error: bad flags or arguments |
3 | Config error: schema missing or unparseable |
Output
Data goes to stdout and diagnostics to stderr, so dottyenv composes in pipes. Values of variables marked secret = true are never printed; output reports presence and validity only. Non-secret values are shown, because seeing "verbose" is how you fix LOG_LEVEL.
Shell completions
$ dottyenv completions zsh > ~/.zfunc/_dottyenv $ dottyenv completions bash > /etc/bash_completion.d/dottyenv $ dottyenv completions fish > ~/.config/fish/completions/dottyenv.fish
powershell and elvish are also supported.