Penv Cloud
Start free
Docs
Referencesince 1.0.0-alpha.3

The .env.schema format

What penv adds to the @env-spec grammar in .env.schema: decorators, computed values, inference, grammar version, diagnostics and the generated .env.

.env.schema
# @penv=acme/api-gateway
# @currentEnv=$APP_ENV

# @type=enum(development,staging,production)
APP_ENV=development

# @type=url
DATABASE_URL=

# @type=string(startsWith=sk_) @rotate=90d @hosts(api.stripe.com)
STRIPE_SECRET_KEY=

# @type=url @sensitive=false
NEXT_PUBLIC_APP_URL=http://localhost:3000

The grammar is @env-spec; upstream meanings: root decorators, item decorators. This page lists what penv 1.0.0-rc.1 adds, restricts or infers. Concept: schema and types.

penv reads the nearest .env.schema upward from your working directory, so a monorepo holds one per app. A # --- line ends a block.

Decorators

DecoratorPlacementValue penv acceptsOrigin
@penvHeaderorg/project or <provider>:org/projectpenv
@schemaHeaderWhole number above zero (legacy, see grammar version)penv
@defaultSensitiveHeaderNothing, true or false@env-spec
@defaultRequiredHeaderNothing, true, false or infer@env-spec
@currentEnvHeaderA key, such as $APP_ENV@env-spec
@importHeader(path, KEY, ...) or (path, pick=[KEY, ...]); a schema file, never a value file@env-spec
@assertHeader or key(expression, "message")penv
@required, @optionalKeyNothing, true, false or forEnv(env, ...)@env-spec
@sensitiveKeyNothing, true or false@env-spec
@typeKeyOne of the seven types@env-spec
@example, @docsKeyA value, required@env-spec
@deprecatedKeyNothing, or a note@env-spec
@dynamic, @staticKeyNothing. Parsed and kept; penv acts on neither@env-spec
@hostsKeyOne host, or (a.com, "*.b.com"): at most 32, no scheme, port, path or bare *penv
@rotateKeyA duration, requiredpenv

penv ignores anything else with a warning, so a varlock schema still parses: varlock_only for @plugin and for other values of @required, @optional or @sensitive; unknown_decorator for the rest. penv has no @scope: read access is a role in the console.

The header is the first comment block; a block on a key is still the header when it carries a header decorator, and that key gets no decorators.

@penv

@penv takes an optional provider, then org/project with no second /; anything else is invalid_header. No @penv means local mode.

PartRule
<provider>:Lowercase slug. penv 1.0.0-rc.1 has one provider, penv (us, at https://penv.cloud); another is unknown_provider. --provider overrides it.
[providers.<slug>] urlThe API root, in .penv/config.toml. penv sends a root named only there no PENV_TOKEN, CI or AWS proof (credential_withheld); PENV_URL wins (variables).

@hosts

Under an agent, or with penv run --sealed, penv run hands the command a placeholder and puts the value into requests to the @hosts hosts only. A type with no room for a placeholder is cannot_seal (errors).

@hosts may be computed:

# @hosts=url($DATABASE_URL, host)
DATABASE_URL=
.penv/config.toml
[hosts]
DATABASE_URL = ["db.internal.test"]

penv check and a sealed run refuse a key whose hosts differ from its pin; penv check --pin-hosts writes the pins. penv push sends a computed @hosts to penv.cloud as hostsExpr, the expression as written. We store and show it and enforce nothing; sealed runs and [hosts] pins enforce it on your machine. Config.

@assert

# @assert(not(eq($PORT, $ADMIN_PORT)), "PORT and ADMIN_PORT collide")

A false expression fails penv check and penv run with the message. Expressions use the functions below.

Grammar version

.penv/config.toml
[schema]
version = 1

penv init writes the version here rather than as @schema in your file, because varlock rejects @schema (root decorators). penv 1.0.0-rc.1 reads up to version 1.

A higher version is unsupported_schema (config) or schema_too_new (@schema); penv upgrade fixes both. The config wins over @schema.

Types

penv accepts seven of the upstream data types. A key with no @type is a string.

TypeConstraints penv accepts
stringstartsWith, endsWith, minLength, maxLength, matches
numbermin, max, isInt, precision
boolean, url, emailnone
portmin, max
enum(a,b,c)none; bare arguments are members

There is no integer; write number(isInt=true). Another type is an unknown_type warning and penv checks a string; an unknown constraint is an unknown_constraint warning and is ignored.

penv keeps matches and precision and passes them to us and to your generated types, but enforces neither (it carries no regular expression engine): a matches constraint never fails a value at penv check.

Durations

@rotate takes y, m (calendar months), w, d, h, min and s, largest first, each at most once: 90d, 1y6m, 30min. 90 days does not parse. penv check prints @rotate as a reminder counted from the value's last write, without changing its exit code.

Computed values

.env
DATABASE_URL=postgres://app:${DB_PASSWORD | urlencode}@db:5432/app
LOG_LEVEL=${LEVEL:-info}
SESSION_SECRET=random(length=32)
SHARED_KEY=penv(production/SHARED_KEY)
LITERAL='${NOT_EXPANDED}'

A value or schema default that holds ${KEY} or is one function call is computed. Upstream semantics: functions.

FormOriginpenv behavior
${KEY}, ${KEY:-or} (empty or unset), ${KEY-or} (unset only)@env-specA reference nothing sets computes as empty, with a warning. \$ is a literal dollar.
ref, concat, fallback, if, eq, not, isEmpty, forEnv@env-specEvaluated.
${KEY | urlencode}, base64, b64d, lower, upper, trimpenvFilters, left to right. b64d decodes base64.
match(subject, label: value, _: value), and, or, startsWith, endsWithpenvEvaluated.
random(kind=, length=)penvkind is alnum, hex, base64url or uuid; length is 8 to 512. penv run generates it once and keeps it in .env.local (.env.test.local for test); penv push never sends it. random(32) fails: write random(length=32).
url(), json(), hash()penvRead, write and build URL parts, JSON and hashes. Functions.
penv()penvpenv(KEY), penv(env/KEY), penv(project/env/KEY) or penv(org/project/env/KEY) reads that value from us; penv() alone reads the key it sits on.
exec()varlockRefused: a schema never starts a process.
Any other functionvarlock pluginRefused; single-quote the value to keep it as written.

A single-quoted or backticked value is never computed. A value computed from a sensitive key is sensitive; a bundler-public one fails penv check and penv run. Failures exit 3 (errors).

Inference

penv decides required and sensitive per key when no decorator says otherwise.

PropertyRule
RequiredKEY= (nothing after =) is required. KEY=value makes value the default and the key optional. @defaultRequired changes the default.
SensitiveEvery key is sensitive unless it carries @sensitive=false, the header says @defaultSensitive=false, or the name starts with a bundler prefix: NEXT_PUBLIC_, VITE_, PUBLIC_, EXPO_PUBLIC_, NUXT_PUBLIC_, REACT_APP_, GATSBY_, VUE_APP_, STORYBOOK_, or one you list under [public] prefixes in .penv/config.toml.

A required key with no value fails penv check. penv ls prints no values, and penv run scrubs sensitive values from its output. What penv init infers: from a .env file.

A bundler-prefixed key marked @sensitive fails with sensitive_public_key: the prefix ships the value in the client bundle, so penv cannot keep it secret. Drop the decorator or rename the key.

Diagnostics

penv check reports every problem with line and column and exits 3; other commands refuse the file as invalid_schema, exit 3. Warnings never change the exit code.

DiagnosticCauseFix
invalid_lineA line that is neither a # comment nor KEY=value.Comment it out or make it KEY=.
invalid_key_nameA name starting with a digit, or holding more than letters, digits and _.Rename it.
duplicate_keyThe same key is declared twice.Keep one block.
invalid_decoratorA non-decorator on an @ line, an unclosed @name(, or a malformed @assert.Fix the line.
missing_decorator_value@example, @docs, @rotate or @type with nothing after =.Add the value.
invalid_decorator_valueA non-boolean on a boolean decorator, a value on @dynamic or @static, a bad @rotate, or a bad or repeated @hosts entry.Correct it.
unterminated_quoteA quoted decorator value is never closed.Close the quote.
invalid_header@penv, @schema, @currentEnv or @import without its value.Correct the header.
invalid_importThe imported file is missing, a value file, circular, or lacks a picked key.Import a schema.
schema_too_new, unsupported_schema@schema or [schema] version is above what this build reads, or not a whole number.penv upgrade
invalid_config.penv/config.toml is not valid TOML.Fix the named line.
invalid_typeA missing ), a bare argument on a non-enum type, or an empty enum.Correct it.
duplicate_constraintThe same constraint given twice.Keep one.
sensitive_public_keyA bundler-prefixed or [public]-prefixed key marked @sensitive.Drop @sensitive or rename the key.
hosts_unpinnedA sealed run found a computed @hosts with no pin, or hosts that differ from it.Run penv check, then penv check --pin-hosts once the hosts are right.
hosts_uncomputedpenv check --pin-hosts could not compute a key's hosts; nothing was pinned.Fix the @hosts it names, then pin again.

Undeclared keys in .env are drift: penv check names them, penv run masks them, and the exit code holds.

Generated .env

penv pull and penv set write the same plain lines:

PropertyValue
EncodingUTF-8, no byte order mark, LF; a CRLF inside a value is written as LF
LinesKEY=value, upper snake case, no export, no spaces around =, no duplicates. penv pull adds a # penv:redacted KEY comment for each key we withhold.
Plain valueUnquoted
#, ', a backtick or whitespaceDouble quotes
Line breaksDouble quotes, each break as \n
", \, $ or a function callSingle quotes, read literally. penv never expands a written value.
A ' with any of those; a carriage return; a line break with " or \Refused as unwritable_value; read it with penv run

The reader accepts more and warns with line and column, never the character.

Reader outcomeInput
Read, with a warningByte order mark, CRLF endings, export prefix, spaces around =, lowercase name, duplicate key (the last wins), comment after a value, value spanning lines, unclosed quote, the escapes \r, \t, \" and \\ (decoded)
Dropped, with a warningUnusable key name, line with no =

Next: penv check.