The .env.schema format
Every line of .env.schema: the header, the decorators, the types and constraints the parser accepts, and what penv check refuses.
.env.schema is the one penv file you commit. This page is the grammar the parser accepts, line by
line. For why the file exists, read the schema and your types.
# @penv=acme/api-gateway @schema=1
# @defaultSensitive=true
# The primary Postgres connection.
# @type=url
DATABASE_URL=
# @type=string(startsWith=sk_) @rotate=90d
STRIPE_SECRET_KEY=
# @type=url @sensitive=false
NEXT_PUBLIC_APP_URL=http://localhost:3000
# @type=enum(development,staging,production)
NODE_ENV=developmentpenv reads the nearest .env.schema upward from the working directory. A monorepo holds one per
app. A byte order mark is stripped, and both LF and CRLF line endings read the same.
The header
The header is the first comment block in the file. A block that sits directly on a key is still the header when it carries one of these four, and in that case the key below it gets no decorators of its own.
| Decorator | Takes | What it does |
|---|---|---|
@penv | org/project | Names the cloud project. Absent means local mode. |
@schema | A whole number above zero | The grammar version the file was written with. |
@defaultSensitive | Nothing, true or false | Flips the sensitivity inference for every key. |
@defaultRequired | Nothing, true or false | Flips the requiredness inference for every key. |
@penv splits on the first /. Both halves must be there and the project half may not hold a
second /. Anything else is invalid_header.
This build reads up to @schema=1. A file that says more is schema_too_new, and the message names
penv upgrade.
Any other decorator in the header block is unknown_decorator.
A key block
Comment lines directly above a key are its block. A blank line ends the block.
- A comment line starting with
@carries decorators. One line may carry several, separated by spaces. - A comment line that does not start with
@is the key's description. Several such lines join with one space between them. - A decorator is
@nameor@name=value. It is never positional and the order never matters. - A key line is
NAME=orNAME=default. The name starts with an ASCII letter or_and continues with letters, digits or_.
Quote a decorator value that holds whitespace or a #. Inside double quotes a backslash escapes the
next character. Inside single quotes nothing is an escape. An unquoted value runs to the next space
that is not inside quotes or parentheses.
Types
@type is optional. A key without it is a string.
| Type | A value passes when it is | Constraints it takes |
|---|---|---|
string | Any text | startsWith, endsWith, minLength, maxLength, matches |
number | A finite number | min, max, isInt, precision |
boolean | true, yes, 1, false, no or 0, in any case | none |
url | A scheme, :// and a host with no spaces | none |
email | Text, an @, then a domain holding a dot | none |
port | A whole number from 1 to 65535 | min, max |
enum(a,b,c) | Exactly one of the members you list | none |
There is no integer type. A whole number is number(isInt=true).
Constraints go inside the parentheses as name=value, comma separated, and spaces around them are
allowed: @type=string(startsWith=sk_, minLength=20). Bare arguments are enum members and are
refused on every other type. An enum with no members is refused.
matches and precision are parsed and kept, and the binary enforces neither. penv carries no
regular expression engine, so a matches constraint reaches the cloud and your generated types
without ever failing a value at penv check.
Required and sensitive are worked out for you
Required. A key with nothing after the = is required. A key with a value on the line takes that
value as its default and becomes optional. @required and @optional say otherwise, and neither
takes a value. A required key with no value and no default fails penv check.
Sensitive. Every key is sensitive unless it carries a bundler prefix or you write
@sensitive=false. The six prefixes are NEXT_PUBLIC_, VITE_, PUBLIC_, EXPO_PUBLIC_,
NUXT_PUBLIC_ and REACT_APP_. @sensitive on its own means @sensitive=true.
Sensitivity decides whether penv ls masks a value and whether
penv run scrubs it out of your program's output.
A key that is bundler-prefixed and marked @sensitive fails penv check with
sensitive_public_key. The prefix puts the value in your client bundle, so calling it sensitive
would be a claim penv cannot keep. Drop the decorator or rename the key.
The rest of the vocabulary
penv follows the @env-spec vocabulary word for word, so a file penv wrote reads the same to a varlock user.
| Decorator | Takes | What it does |
|---|---|---|
@example | A value, required | A sample value for this key. |
@docs | A value, required | A link to whatever documents this key. |
@deprecated | Nothing, or a note | Marks a key on its way out. |
@dynamic / @static | Nothing | The spec's pair. penv keeps them and acts on neither. |
Three decorators are penv's own, for things the spec has no word for.
| Decorator | Takes | What it does |
|---|---|---|
@since | A version, required | The release that introduced this key. |
@rotate | A duration, required | How often this value should be replaced. |
@dynamicFrom | An engine name, required | The cloud mints this value on request. |
A duration is digits followed by one of s, m, h, d or w, so 90d and 12h both parse and
90 days does not.
There is no @scope. Who may read a key is a role in the console, and it lives with the environment
rather than in a file anyone can edit. There are no aliases either: one concept has one name, and a
decorator penv does not know is an error rather than a line it passes over.
What penv check refuses
Every problem in the file is reported at once, each with a line and a column.
| Diagnostic | What it means |
|---|---|
invalid_line | A line that is neither a # comment nor KEY=value. |
invalid_key_name | The name starts with a digit, or holds something outside letters, digits and _. |
duplicate_key | The same key is declared twice. Keep one block. |
invalid_decorator | Something on an @ line that is not a decorator. |
unknown_decorator | A decorator penv does not know, or a header decorator on a key. |
missing_decorator_value | @example, @docs, @since, @rotate, @dynamicFrom or @type with nothing after the =. |
invalid_decorator_value | A boolean decorator given something other than true or false, a value on @required, @optional, @dynamic or @static, or a @rotate that is not a duration. |
unterminated_quote | A quoted decorator value that is never closed. |
invalid_header | @penv without org/project, or @schema without a version number. |
schema_too_new | The file's @schema is above what this build reads. Run penv upgrade. |
unknown_type | A @type name outside the seven above. |
invalid_type | A missing closing bracket, a bare argument on a type that is not an enum, or an enum with no members. |
unknown_constraint | A constraint this type does not take. |
duplicate_constraint | The same constraint given twice. |
sensitive_public_key | A bundler-prefixed key marked @sensitive. |
penv check lists these itself and exits 3. Every other command refuses the file with the single
code invalid_schema at the same exit, and tells you to run penv check for the list.
Keys present in .env that the schema does not declare are drift. penv check names them and
penv run masks them, and neither changes the exit code.
The generated .env
penv pull writes a plain file, and so does the one you keep in local mode. It is UTF-8 without a
byte order mark, LF line endings, KEY=value with upper snake case names, no export, no spaces
around the =, no comments and no duplicates.
Quoting is the subset that Node's util.parseEnv and dotenv both read back, and no more. Inside
double quotes \n is an escape and nothing else is, which is how a multi-line value such as a PEM
key sits on one line.
| The value holds | penv writes it |
|---|---|
Nothing but plain text, no whitespace, #, quote or backslash | Bare |
Whitespace, a # or a ', and no line break | Double quoted, nothing escaped |
A " or a \, and no line break | Single quoted, where nothing is an escape |
A line break, and no " or \ | Double quoted, with the breaks written as \n |
A \r\n is folded to \n before any of that. Three values have no portable spelling left and penv
refuses to write them rather than mangle them: a value holding a lone carriage return, a value that
mixes a line break with a " or a \, and a value holding both a " or \ and a '. Keep those
in the cloud and read them with penv run.
A $ is refused outright, in every position. penv never expands a value, so a file carrying one
would read differently everywhere else.
Reading is more forgiving than writing, because the file on your disk was written by something else.
penv decodes \r, \t, \" and \\ as well, and warns once per value, naming the line and the
column and never the character. It also warns on a byte order mark, CRLF endings, an export
prefix, spaces around the =, a lowercase name, a $, a duplicate key, a comment after a value, a
value spanning lines and a quote that is never closed. Two more warnings drop the line rather than
read it: a name that is not a usable key name, and a line carrying no = at all.
Next: penv check.