Docs
Referencesince 1.0.0-alpha.3

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.

.env.schema
# @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=development

penv 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.

DecoratorTakesWhat it does
@penvorg/projectNames the cloud project. Absent means local mode.
@schemaA whole number above zeroThe grammar version the file was written with.
@defaultSensitiveNothing, true or falseFlips the sensitivity inference for every key.
@defaultRequiredNothing, true or falseFlips 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 @name or @name=value. It is never positional and the order never matters.
  • A key line is NAME= or NAME=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.

TypeA value passes when it isConstraints it takes
stringAny textstartsWith, endsWith, minLength, maxLength, matches
numberA finite numbermin, max, isInt, precision
booleantrue, yes, 1, false, no or 0, in any casenone
urlA scheme, :// and a host with no spacesnone
emailText, an @, then a domain holding a dotnone
portA whole number from 1 to 65535min, max
enum(a,b,c)Exactly one of the members you listnone

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.

DecoratorTakesWhat it does
@exampleA value, requiredA sample value for this key.
@docsA value, requiredA link to whatever documents this key.
@deprecatedNothing, or a noteMarks a key on its way out.
@dynamic / @staticNothingThe spec's pair. penv keeps them and acts on neither.

Three decorators are penv's own, for things the spec has no word for.

DecoratorTakesWhat it does
@sinceA version, requiredThe release that introduced this key.
@rotateA duration, requiredHow often this value should be replaced.
@dynamicFromAn engine name, requiredThe 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.

DiagnosticWhat it means
invalid_lineA line that is neither a # comment nor KEY=value.
invalid_key_nameThe name starts with a digit, or holds something outside letters, digits and _.
duplicate_keyThe same key is declared twice. Keep one block.
invalid_decoratorSomething on an @ line that is not a decorator.
unknown_decoratorA 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_valueA 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_quoteA quoted decorator value that is never closed.
invalid_header@penv without org/project, or @schema without a version number.
schema_too_newThe file's @schema is above what this build reads. Run penv upgrade.
unknown_typeA @type name outside the seven above.
invalid_typeA missing closing bracket, a bare argument on a type that is not an enum, or an enum with no members.
unknown_constraintA constraint this type does not take.
duplicate_constraintThe same constraint given twice.
sensitive_public_keyA 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 holdspenv writes it
Nothing but plain text, no whitespace, #, quote or backslashBare
Whitespace, a # or a ', and no line breakDouble quoted, nothing escaped
A " or a \, and no line breakSingle 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.