Env file diff checker
Paste a .env.example and your own .env to see which keys are missing, extra, defined twice, empty, or malformed. You also get a paste-ready block of the missing keys, with the example’s defaults or names only. The check compares key names and emptiness, never values, and it runs in your browser, so nothing you paste is sent anywhere.
Worked example
--- example ---
# Server
PORT=3000
NODE_ENV=development
# Database
DATABASE_URL=postgres://app:app@localhost:5432/app_dev
REDIS_URL=redis://localhost:6379
# Services
STRIPE_SECRET_KEY=
SESSION_SECRET=change-me
LOG_LEVEL="info" # debug, info, warn, or error
--- actual ---
PORT=3000
NODE_ENV=development
export DATABASE_URL="postgres://app:app@localhost:5432/app_dev"
STRIPE_SECRET_KEY=
PORT=4000
DEBUG_SQL=1
log_level=debug
cache.ttl=60
SESSION SECRET=local-onlyEnv file diff
Compares key names and emptiness only. Values from the actual file are not part of this report.
Summary
- Example file: 7 keys
- Actual file: 7 keys
- Missing from the actual file: 3
- Extra in the actual file: 3
- Duplicate keys in the actual file: 1
- Empty values in the actual file: 1
- Malformed lines in the actual file: 1
Missing from the actual file (3)
- REDIS_URL (example line 7, has a default)
- SESSION_SECRET (example line 11, has a default)
- LOG_LEVEL (example line 12, has a default)
Extra in the actual file (3)
- DEBUG_SQL (line 6)
- log_level (line 7)
- cache.ttl (line 8)
Possible case mismatches (1)
- LOG_LEVEL is missing, but the actual file has log_level on line 7
Duplicate keys in the actual file (1)
- PORT on lines 1 and 5
Empty values in the actual file (1)
- STRIPE_SECRET_KEY (line 4, also empty in the example file)
Malformed lines in the actual file (1)
- Line 9: The key contains a space or tab. Expected KEY=value with no whitespace inside the key.
Warnings (1)
- Actual file, line 8, cache.ttl: Not a portable shell variable name: contains a dot. POSIX shell names use only letters, digits, and underscores and do not start with a digit.
Paste-ready block (names and example defaults)
REDIS_URL=redis://localhost:6379
SESSION_SECRET=change-me
LOG_LEVEL="info"Read the report from the summary down. Three keys are missing, so the block at the end holds REDIS_URL and SESSION_SECRET with their example defaults, and LOG_LEVEL with its quoted default. The inline comment after LOG_LEVEL in the example is not copied.
The case check notices that the actual file has log_level where the example has LOG_LEVEL. Those are different names. PORT is defined on lines 1 and 5, so one definition should go. STRIPE_SECRET_KEY is empty in both files, which usually means it still needs a real value.
The line SESSION SECRET=local-only is malformed because the key contains a space, which is why SESSION_SECRET also shows as missing. The key cache.ttl is flagged because a dot is not portable in a shell variable name. DATABASE_URL counts as present even though the actual file writes it with export and quotes.
How to use it
Paste the example file
Paste the contents of
.env.example,.env.sampleor.env.templateinto the example box. This file should hold key names and safe placeholder values, never real secrets.Paste your actual file
Paste your
.envinto the actual box. If you would rather not paste real values, replace each non-empty value withxand leave empty values empty. The check only needs key names and whether a value is empty.Choose how to build the block
Leave names only off to copy the example’s defaults into the block. Turn it on to get
KEY=lines with nothing after the equals sign. Use names only if the example file might hold anything private.Read the sections
Start with missing keys, then duplicates, empty values, and malformed lines. Extra keys are often fine: they may be local overrides, or the example may be out of date.
Add the missing keys
Copy the paste-ready block, add it to the end of your
.env, and fill in real values. Check that the last line of the file ends with a line break first, or the first pasted key joins it.Check again
Run the check once more. If you added a key the app needs, add it to
.env.exampletoo, so the next person starts from a complete file.
How it works
The checker reads both files with one parser, then compares key names. These are the rules, in the order they apply.
- Lines. CRLF, LF, and a lone CR each end a line, and a leading byte order mark is ignored. Blank lines are skipped. A line whose first non-blank character is
#is a comment. - Export prefix. An optional
exportfollowed by a space or tab is ignored, as python-dotenv documents. - Keys. A key is the text before the first
=, ignoring spaces and tabs around it. It may use ASCII letters, digits, underscores, dots, and dashes, and any other character, including a non-ASCII letter, makes the line malformed. A key with a dot or dash, or one that starts with a digit, is flagged as non-portable, because the POSIX definition of a shell name allows only letters, digits, and underscores and forbids a leading digit. - Values. A value is the text after the first
=, so it can contain more=characters. Unquoted values lose surrounding spaces and tabs. - Inline comments. In an unquoted value, a
#starts a comment only when a space or tab comes before it. That is the python-dotenv rule, and the Docker Compose env file format documents the same rule for a space. Node’s dotenv starts a comment at any#in an unquoted value, so the Node option applies that rule instead. Inside quotes a#is part of the value, and a comment may follow the closing quote. - Quotes. Single and double quoted values keep their spaces. A backslash escapes the next character, so an escaped quote does not close the value. Line breaks are allowed only inside double quotes. Node’s dotenv supports multi-line values from version 15. python-dotenv documents them in single quotes too, and the Node and Docker Compose parsers read them there as well, but this checker reports them as malformed. A quote that never closes is reported as malformed, and parsing resumes on the next line.
- Empty values. A value is empty when nothing is left after quotes and comments are removed.
KEY=,KEY="", andKEY= # noteare empty.KEY=" "is not. - Duplicates. Every line that defines the same key is listed. The empty check and the pasted default use the last definition, and the checker does not claim your loader does the same.
- Comparison. Keys match by exact name, so
Api_KeyandAPI_KEYdiffer. A missing key is in the example and not in the actual file. An extra key is the reverse.
The actual file is parsed without keeping values. The report shows only its key names, line numbers, and whether each value is empty. Nothing else from that file appears in the report, in an error message, or in the paste-ready block, which holds example defaults only.
Rig’s env_diff MCP tool draws the same line: it compares key names only and never returns values. It uses Rig’s own parser, so results can differ on unusual files.
Limitations
- It compares key names and emptiness only. It does not check that a value is valid, and a placeholder such as change-me counts as filled in.
- References to other variables are not expanded, so a value that points at an unset variable still counts as filled in. Docker Compose expands them in unquoted and double-quoted values, not in single-quoted ones.
- Loaders do not all agree. Inline comments follow python-dotenv by default, with a Node option. Docker Compose reads KEY= # note as a value and does not end a value at a tab before #. Backticks are not quotes, and KEY: value and bare KEY lines are malformed.
- It sees only the two files you paste, up to 200 KB and 5,000 lines each. A larger file is rejected, not truncated. Keys your app gets from .env.local, a shell export, a process manager, or CI secrets show up as missing here.
- A multi-line value needs double quotes. Without them, each line is read on its own, so a line of a pasted private key can appear as a key name in the report. Replace such values with x before you paste.
Practical applications
Use it whenever two env files should agree and you cannot see why they do not.
- Onboarding. A new teammate copies
.env.exampleto.envand the app still fails. The diff shows which keys they never filled in. - After a pull. A branch added a config key. Compare your
.envwith the updated example to find what to add before the server starts. - Reviewing a pull request. A change reads a new variable. Check that
.env.examplegained the same key. - Works on my machine. Paste a working
.envas the example and the broken one as the actual file to see exactly what differs. - Cleaning up. Duplicates and empty values hide in long files. The report lists the line numbers.
- Docker Compose and CI. Files read through an
env_fileentry follow similar rules, so the malformed and duplicate checks still help.
Common mistakes
- Pasting the real .env into the example box. The paste-ready block copies the example’s defaults, so real values would appear in it. Use names only if you are not sure what the example file holds.
- Treating every extra key as an error. An extra key is often a local override or a debugging flag. Decide whether it belongs in the example file before you remove it.
- Writing a value that contains a space and a # without quotes. After a space, # starts a comment, so ab #cd is read as ab. Quote any value that contains a #.
- Fixing a missing key in your own .env and leaving .env.example behind. When you add a key the app needs, add it to the example file in the same change.
- Assuming the last duplicate always wins. Which definition wins is up to the loader, so delete the one you do not want.
Frequently asked questions
What is the difference between .env and .env.example?
The .env file holds the real values for your machine and stays out of version control. The .env.example file lists the same key names with placeholders or safe defaults, and it is committed, so anyone who clones the project knows which keys to set.
Does this tool send my .env file anywhere?
No. The comparison runs in your browser and makes no network request. The report lists key names and line numbers, never values from your actual file. Even so, avoid pasting production secrets into any web page. Replace non-empty values with x first, because the check needs only key names and whether a value is empty.
Why is a key reported as missing when it is in my .env?
Check four things. The name may differ in letter case, because keys are case sensitive. The line may be commented out with a leading #. The line may be malformed, which the report lists with a line number. Or the key may sit inside a multi-line quoted value, where it is text, not a key.
Which duplicate definition wins?
It depends on the program that reads the file, so this checker does not guess. It lists every line that defines the key. Keep the definition you want and delete the others, so every loader sees the same value.
Why does the checker warn about dots and dashes in key names?
A POSIX shell name uses only letters, digits, and underscores and does not start with a digit. A loader may accept a key such as cache.ttl, but a shell cannot use it as a variable name, so the file breaks when anything sources it. The warning is advice, not an error.
Should I commit .env.example?
Yes. It holds key names and harmless placeholders, so it is safe to share and it documents what the app needs. Keep real secrets out of it. The dotenv documentation advises against committing the real .env file.
Sources
Env profiles per lane in Rig
Rig keeps an env profile for each lane and masks values in its interface. Its env_diff MCP tool also compares key names only, so an agent can see which keys are missing without reading a secret.