BV
All tools
developer

.env Manager

Validate environment variable names and values before they reach production. Env Manager checks names against POSIX rules, flags values that different .env parsers read differently, and shows which entries are secrets that should not sit in a file.

Muhammad Bilal
Muhammad Bilal Virk
14 min read
Live tool

Environment variables have a specification. The .env file does not. Paste yours in and Env Manager checks each name against the POSIX rules, flags the values that python-dotenv, Node dotenv and bash will each read differently, and marks the entries that are credentials rather than configuration.

What this tool does and who it is for

Paste a .env file into Env Manager and it checks three separate things: whether each variable name is legal, whether each value will survive being read back by whatever loads it, and whether the entry looks like a credential that should not be sitting in a plain text file at all. Everything runs in your browser. Nothing is uploaded, which matters more here than on most tools, because the input is by definition your secrets.

It is built for two people. The first has just cloned a project whose .env.example is several commits out of date, and wants to know what is missing before the application crashes on startup. The second has a build that works locally and fails on the host, and has not yet worked out that the file is being read by two different parsers with two different sets of rules.

.env Manager — illustration

That second case is the real subject of this page, so it is worth stating plainly at the top. Environment variables have a specification. The .env file does not. Chapter 8 of the Open Group Base Specifications Issue 8, which is IEEE Std 1003.1-2024, defines exactly what a variable name may contain and what a value may hold. No standard anywhere defines the file that sets them. Every library that reads one invented its own rules, and those rules disagree with each other.

How to read the output, with a worked example

Below is a nine line .env file. Nothing in it is contrived. Every line is a shape you will find in real projects.

text
DEBUG=false
PORT=8000
DB_PASSWORD=p@ss#word
GREETING=hello world
QUOTED="hello world"
EMPTY=
SPACED = value
export EXPORTED=yes
PATH_LIKE=/usr/bin:$PATH

I loaded that exact file three ways on the same machine and recorded what each one returned: python-dotenv 1.2.2 through dotenv_values('.env'), Node's dotenv 17.4.2 through config({ processEnv: {} }), and bash through set -a; . ./.env; set +a. Five of the nine lines agreed. Four did not.

The password lost six characters and nothing warned me

DB_PASSWORD=p@ss#word is an unremarkable password. python-dotenv returned p@ss#word. bash returned p@ss#word. Node's dotenv returned p@ss.

The value was cut from ten characters to four. There is no warning, no error, and no entry in any log. The application starts, connects to the database with a four character password, and fails authentication. The stack trace says the credentials are wrong. That is true, and it tells you nothing about why.

This is documented behaviour rather than a bug. The dotenv README states that "# marks the beginning of a comment (unless when the value is wrapped in quotes)", and adds that "Comments begin where a # exists, so if your value contains a # please wrap it in quotes. This is a breaking change from >= v15.0.0 and on." So the fix is one character on each end:

text
DB_PASSWORD="p@ss#word"

That returns the full string in all three readers. It is also a fix nobody finds by rereading their own file, because the file looks correct. The # is not a typo. It is a character the password generator chose, and there is nothing on the line to suggest the parser will treat it as punctuation. If you generate credentials with the password generator, the symbol set it draws from includes #, so this is not a rare collision. It is a routine one.

bash rejects two lines that both libraries accept

GREETING=hello world came back as hello world from python-dotenv and from Node. Sourcing the same line in bash produced ./.env: line 4: world: command not found and left the variable empty, because to a shell that line is an assignment followed by a command.

SPACED = value did the same thing, reporting SPACED: command not found. Both libraries stripped the spaces and returned value, which the dotenv README confirms as a rule: "whitespace is removed from both ends of unquoted values".

This becomes expensive the moment something sources the file rather than parsing it, which is what a Docker entrypoint script, a cron wrapper or a CI step usually does. The same file is valid inside your application and broken inside your deployment. Worse, the shell's failure is non fatal by default. The line errors, the variable stays empty, and the script carries on.

bash expands what the libraries keep literal

PATH_LIKE=/usr/bin:$PATH came back from both libraries as the literal string /usr/bin:$PATH, dollar sign intact. bash expanded it into /usr/bin: followed by the real search path. Neither behaviour is wrong. They are simply different, and only one of them is what you intended.

dotenv is explicit that it does not do this: its documentation points variable expansion and command substitution out to separate packages rather than supporting either. So a value like DATABASE_URL=postgres://$USER@localhost/db is a working connection string under bash and a broken one under Node, from the same line of the same file.

The full result table

Line in the file python-dotenv 1.2.2 Node dotenv 17.4.2 bash . ./.env
DEBUG=false false false false
PORT=8000 8000 8000 8000
DB_PASSWORD=p@ss#word p@ss#word p@ss p@ss#word
GREETING=hello world hello world hello world not set, line errors
QUOTED="hello world" hello world hello world hello world
EMPTY= empty string empty string empty string
SPACED = value value value not set, line errors
export EXPORTED=yes yes yes yes
PATH_LIKE=/usr/bin:$PATH literal $PATH literal $PATH expanded

Read that table as a failure rate rather than a curiosity. Four lines in nine behaved differently depending on who read the file, and in three of those four cases the disagreement was silent. Only GREETING and SPACED under bash produced any output at all, and that output went to stderr in a deployment script nobody reads unless something has already broken.

The rule that falls out of it is short: quote every value that is not a bare number, and never rely on interpolation inside a .env. Quoting costs nothing and removes three of the four divergences. Interpolation is the one you cannot fix by quoting, so resolve it in code where you can see it.

The rules that are actually specified

The file has no standard. The variables it sets do, and six rules from Chapter 8 are worth knowing, because breaking them produces failures that look like something else entirely.

Names use uppercase letters, digits and the underscore, and do not begin with a digit. The specification says names "consist solely of uppercase letters, digits, and the ", and adds a note that other applications "may have difficulty dealing with environment variable names that start with a digit. For this reason, use of such names is not recommended anywhere." A name like 2FA_SECRET is a name a shell cannot assign to.

A name may not contain the = byte. That is what makes the format parseable at all: the first = on the line is the separator, always.

A value is an arbitrary sequence of bytes except the null byte. So a value can hold almost anything, including newlines and quotes, which is why a multiline private key can live in a .env at all. The constraint is on the file format around it, not on the value itself.

The environment plus the arguments share a single size ceiling, {ARG_MAX}. Paste a large base64 certificate into a variable and you are spending from the same budget the command line spends from. This is the failure mode behind an Argument list too long error that appears after nothing changed except a config value.

If more than one string in the environment has the same name, the consequences are undefined. This is the one that actually bites, because a duplicated key is the normal outcome of a merge. The specification does not promise which one wins, and implementations differ, so a duplicate is not "last one wins" that you can rely on. Env Manager flags duplicates for that reason. Note that dotenv's own collision rule is separate and also worth knowing: by default it "will never modify any environment variables that have already been set", so anything already in the real environment beats your file unless you pass override: true.

Case is never folded, and lowercase is reserved for you. Path and PATH are two different variables. The specification reserves the lowercase namespace for applications, and lists the names it is "unwise to conflict with", among them PATH, HOME, SHELL, TERM, TZ, TMPDIR, PWD, USER, LOGNAME, LANG, EDITOR, PAGER, RANDOM, SECONDS, COLUMNS and LINES. Calling your own variable LINES or RANDOM is legal and will eventually cost you an afternoon.

Common mistakes

Treating values as though they have types. Every value is a string, and every string except the empty one is truthy in both languages people usually reach for. Boolean("false") in JavaScript is true. In Python, bool("false") is True, and so is bool("0"). So DEBUG=false switches debug mode on in any code that writes if (process.env.DEBUG). Compare against an explicit list of accepted strings instead, and fail loudly on anything outside it.

Assuming a missing variable will announce itself. It will not. process.env.MISSING is undefined and os.environ.get("MISSING") is None, and both flow onwards into a request URL or a template until something much later complains. A startup check that reads every required name and exits on the first absent one costs about fifteen lines and converts a mystery into a message.

Keeping one .env and switching it by hand. The Twelve-Factor App's config chapter makes the argument against grouping config into named environments: env vars "are never grouped together as 'environments', but instead are independently managed for each deploy", because the alternative is a combinatorial explosion of staging, qa and joes-staging that makes deployment brittle. Worth reading with its own date in mind: that text was last updated in 2017, and the ecosystem has moved, but the separation argument has aged well and the litmus test it gives is still the sharpest one available. Ask whether your codebase "could be made open source at any moment, without compromising any credentials". If the answer is no, the problem is not the file format.

Committing the file, then fixing it with a delete. Removing a committed .env in a later commit removes it from the working tree, not from history. Anyone with the repository still has the credential, so the only real fix is rotation. Add the file to .gitignore before the first commit, not after the first incident.

Putting real secrets in .env.example. The example file exists to be committed, which means every value in it is public. Give it names and empty values, or obviously fake ones. Env Manager will flag an entry in an example file that looks like a live key.

Using a .env where the platform already has a secret store. A file on disk has no access log, no rotation, no expiry and no scoping. Once a project has more than one person or more than one environment, a managed store earns its keep. That is a separate discipline from validating the file, and if you self host your automation stack it is the first thing to sort out. There is a fuller treatment of the credential side of this in the write up on n8n credentials security.

Frequently Asked Questions

Is the .env format standardised anywhere?

No. There is no RFC, no POSIX chapter and no committee for it. POSIX specifies environment variables, in Chapter 8 of the Base Specifications, and says nothing about a file used to populate them. What exists instead is a set of independent implementations that mostly agree, which is why the disagreements are so hard to spot. The four divergences measured above all come from lines that every reader accepted without complaint.

Should I quote every value?

Quote every value that is not a bare integer. It costs nothing, and it removes the # truncation, the unquoted whitespace difference and the shell's refusal to parse an unquoted space. It does not fix interpolation, because a double quoted value still expands under a shell, so that one has to be handled by not using it.

Is it safe to paste production secrets into this page?

The validation runs in your browser and the values are not sent anywhere. That said, the honest answer is that you should not need to. Run the check against .env.example plus a set of dummy values of the same shape, and you will catch every structural problem on this page without the real keys ever leaving your password manager. A tool that asks for your secrets is a tool worth being sceptical about, including this one.

Can I keep multiline values like a private key in a .env?

Yes, in the libraries. POSIX allows any byte except NUL in a value, and dotenv has supported line breaks inside double quoted values since v15, either literally or via \n escapes. Do not expect a shell to source the same file, and do not expect every language's loader to agree. If a certificate has to travel through configuration, base64 encoding it and decoding in code is the version that behaves the same everywhere, with the {ARG_MAX} ceiling as the limit to watch.

Does a validator catch a wrong value as well as a malformed one?

No, and this is the boundary worth being clear about. A checker can tell you that DATABASE_URL is present, legal, quoted and not obviously a placeholder. It cannot tell you that it points at the staging database. Structural validation and correctness are different problems, and only the first one is automatable from the file alone.

What about .env files in a container image?

Two separate risks. First, a .env copied in by a broad COPY . . becomes a layer in the image, and anyone who can pull the image can read it, regardless of what the running container's environment looks like. Second, a file baked into an image cannot be rotated without a rebuild. Pass configuration at run time and keep the file out of the build context. If you are writing the compose file by hand, the Docker Compose generator will lay out the environment and env_file sections in the right shape, and there is a walk through of the surrounding setup in the n8n self hosted setup guide.

Should configuration live in .env or in a YAML or TOML file?

Different jobs. Secrets and per deploy values belong in the environment, for the reasons the Twelve-Factor argument gives. Structured application configuration that does not vary between deploys, and does need nesting and lists, is better off in a file you can read, which is where YAML and TOML do a job the flat KEY=value format cannot. Trying to express a nested structure in environment variables produces names like SERVICES__0__RETRIES and a parser you then have to maintain.

Where this tool stops

A validator reads a file. That is the whole of its knowledge. It can tell you a name breaks the POSIX rules, that a value will be truncated at a #, that a key is duplicated, or that something looks like a live credential in a file meant to be committed. It cannot tell you the value is the right value, and it cannot tell you which of the three readers above your runtime will actually use, because that answer is in your Dockerfile, your entrypoint script and your framework, not in your .env.

That gap is where a lot of AI generated projects come apart. A model asked to add configuration will reliably produce require('dotenv').config(), a .env.example and a plausible list of variable names. What it will not do is notice that one generated password contains a #, that the entrypoint sources the file the library was written to parse, or that the deploy has no startup check and will happily boot with three empty strings where credentials should be. The code is correct in isolation and the project is not deployable, which is a frustrating combination because nothing in it looks wrong.

That is the work I do most often now: taking something built quickly with an AI assistant and making it survive contact with a real host. Configuration split from code, secrets out of the repository and out of the image, one documented way of loading them that both the application and the deploy script agree on, and a startup check that fails loudly on a missing variable instead of starting up with an empty one. The same pass usually catches the webhook endpoints, which have their own version of this problem, covered in the FastAPI webhook automation write up.

If you have a project that works on your machine and nowhere else, tell me what it does and where it breaks. Most of the time the diagnosis takes one look at the config loading path, and that part costs you nothing.

Muhammad Bilal
Muhammad Bilal Virk
AI automation engineer — building agents, workflows, and RPA that remove repetitive work.
Share
Newsletter

One email, when I ship something worth reading.

No cadence, no filler. Unsubscribe any time.

Free consultation

Want this built against your real numbers?

A 30-minute call to scope the workflow, agent, or automation you actually need.

Book a free consultation

More developer tools

All tools
Next step

Have a workflow that's burning hours every week?

Bring me one real bottleneck. I'll tell you whether it's worth automating, and what it would take.

Book 30 Minutes Call