Articles
Published · October 3, 2026

The .env File Has No Spec: Quoting, Comments, and Multiline Values

There's no official .env file spec. Node, Python, and Docker parse quoting, comments, and multiline values differently — here's what actually holds up.

Quick answer

There's no official specification for the .env file. It looks like one simple format — KEY=value, one pair per line — but Node, Python, and Docker each ship their own parser, and those parsers disagree on comments, quoting, and multiline values often enough to matter. The php-xdg/dotenv-spec project exists specifically because, as its authors put it, there is currently no specification for this file format, and implementations have "slightly divergent behaviours." That's why the same .env file can load cleanly in one tool and silently mangle a value in another. The short version of the env file syntax quoting rules that actually hold up everywhere: quote deliberately, and don't assume a file written for one parser round-trips through another — the same problem shows up wherever one plain-text file meets more than one parser.

How it actually works

Comments: where # stops being a comment

Every parser agrees that a line starting with # is a comment. They disagree on what happens when # shows up mid-value.

# this line is always a comment
FEATURE_FLAG=on#off

Node and npm's dotenv treat any unquoted # as a comment start — FEATURE_FLAG comes out as on, with #off silently dropped. Python's dotenv and Docker Compose only treat a space-prefixed # as a comment, so a hash glued directly to a value survives. Docker's raw CLI parser keeps literal hash characters outright. The rule that holds everywhere: quoting protects anything inside it. FEATURE_FLAG="on#off" comes out as on#off in every parser.

Quoting: unquoted vs. single vs. double

This is the core of the env file syntax quoting rules worth memorizing. A single-quoted value is always literal — no escapes, no variable expansion; python-dotenv's docs note it only recognizes \\ and \'. A double-quoted value recognizes backslash escapes (\n, \t, \\) and, in most parsers, triggers expansion. An unquoted value sits in between — usually interpolated, with no escapes at all.

LITERAL='$HOME/config'      # stays exactly "$HOME/config"
EXPANDED="$HOME/config"     # becomes "/Users/you/config"

If a value genuinely contains a dollar sign — a password hash is the classic case — single quotes are the only format that's safe in every parser tested. This is also where keeping your quotes straight matters in a very literal sense: a curly "smart quote" pasted in from a word processor isn't the same character as ' or ", and no .env parser will treat it as one.

Multiline values

Quoted values can span physical lines in Node, npm's dotenv, and python-dotenv — the value collapses into one string with an embedded newline:

CERT="-----BEGIN CERTIFICATE-----
MIIDXTCCAkWg...
-----END CERTIFICATE-----"

That works for a multi-line secret or certificate directly. It doesn't reliably work everywhere: a filed documentation bug against Docker exists because Docker's own docs didn't accurately describe how its raw CLI parser and Compose's non-interpolated mode handle the same fixture — both can reject a quoted multiline value that Node and Python load without complaint. If you're editing a config file like this and it holds a certificate or multi-line key, test it in the tool that will actually load it first.

When to use it (and when to skip it)

.env files exist as a local-dev convenience, not a portable config standard. The twelve-factor app methodology argues config belongs in environment variables because they're language- and OS-agnostic — a .env file is just a way to simulate that injection on your own machine before a real deploy target provides it natively.

The clearest case where this breaks down is Docker. docker compose's env_file parser strips quote characters from a value before injecting it. docker run --env-file preserves those quote characters literally. Load the identical file both ways and one container sees value, the other sees "value" — quotes included — a mismatch that's been filed against both the Docker CLI and Compose more than once.

Node adds a wrinkle: its built-in --env-file flag parses .env natively, no package required, with multiline support and quote-stripping — but no variable expansion at all, unlike the popular dotenv npm package many Node projects already use.

The practical rule: if a value has to survive more than one parser, keep it unquoted and expansion-free, or quote it and test it in every tool that will read it. And if you move a .env file between Mac and Windows, check for invisible line-ending differences too — a stray \r can break a parser in ways that look identical in a normal text view.

How Carets fits in

None of this is a reason to avoid .env files. It's a reason to look at them instead of guessing. Carets is a native notes and code editor for iPhone, iPad, and Mac, and it's built for exactly this kind of file: syntax highlighting makes a stray smart quote or a glued-on # comment visible instead of invisible, and because Carets organizes everything into files, projects, and tags, your .env file sits right next to the README, the Docker Compose file, and the setup notes it belongs with — not scattered across whatever app happened to be open.

To be clear about what Carets doesn't do: it won't validate .env syntax against Node's parser versus Python's versus Docker's, and it doesn't run your code. What it does is make the file easy to read, edit, and keep organized on whichever Apple device you're on when you need to change it. Try Carets on the App Store for iPhone, iPad, and Mac.

Frequently Asked Questions

Is there an official specification for the .env file format?

No. It's a convention Ruby's dotenv popularized, which Node, Python, and Docker's tooling each then implemented separately. php-xdg/dotenv-spec is an attempt at a formal spec precisely because no official one exists yet.

Why does a # sometimes start a comment and sometimes not?

It depends on the parser and the spacing. Node and npm's dotenv treat any unquoted # as a comment start, even glued to a value. Python's dotenv and Docker Compose only treat a space-prefixed # that way. Quoting the value is the rule that holds everywhere.

Can an env file value span multiple lines?

Usually, but only if it's quoted, and not in every parser. Node, npm's dotenv, and python-dotenv accept a quoted value that breaks across lines. Docker's raw CLI parser and Compose's non-interpolated mode can reject the same fixture — test multiline secrets in the tool that will actually load them.

Why does the same .env file behave differently with docker run vs. docker compose?

Two different parsers. Compose's env_file parser strips quote characters before injecting a value. docker run --env-file preserves them literally — so one container sees the plain value, the other sees it wrapped in quote marks.

Does putting a $VAR reference in a .env file always expand it?

No. Node's native --env-file and the plain dotenv npm package leave $VAR untouched by default; Python's dotenv and Docker Compose expand it by default. Single-quoted values are the one place expansion never happens, in any of them.

Conclusion

.env looks like one simple, universal format. It's a handful of compatible-looking dialects, really — they agree on the easy cases and quietly disagree on comments, quoting, and multiline values. One habit avoids most of the pain: quote on purpose, and test a file in the tool that will load it instead of assuming it round-trips from wherever you wrote it. It's the same lesson plain text teaches everywhere it meets more than one parser — Markdown included.