Docs
Guidesince 1.0.0-alpha.3

Run your app locally

penv run validates your values first, then hands them to one child process and writes nothing to disk.

penv run -- pnpm dev

The command goes after --. penv finds the nearest .env.schema above you, resolves the values, validates them, and starts your command with those values in its own environment. Then it gets out of the way: your command's output is the output, and penv leaves with your command's exit code.

What the child gets

The child inherits your shell environment, then penv overrides it with the resolved values and sets PENV_ENV to the environment it read. A bare command name is looked up along PATH, so npm finds npm.cmd on Windows.

Nothing is written to disk. penv's own messages go to stderr, so stdout stays whatever your command put there. Ctrl-C belongs to the child: penv ignores the interrupt itself so it does not die first and close a pipe it is still cleaning.

On Unix a command killed by a signal exits 128 plus the signal number, which is what a shell reports.

Which environment it reads

--env, then PENV_ENV, then development.

In local mode development is the only environment there is. Any other name is refused with exit code 6, and the message says whether the name came from the flag or from PENV_ENV.

In cloud mode the name is whatever your project calls it, and a machine credential reads only the environment it is bound to.

Where the values come from

ModeHow penv decidesRoughly
LocalParse .env, validate, start the commandUnder a millisecond to find the schema
Cloud, warm cacheOpen the encrypted cache with the keychain keyAbout 2ms
Cloud, revalidatingHEAD the environment with the ETag it holdsAbout 40ms
Cloud, changedGET the environment and rewrite the cacheOne round trip

A development answer stays fresh for 60 seconds without asking the server. Every other environment revalidates on every run, so a change to staging or production lands on the next command rather than a minute later.

Offline, development runs on the cache and warns once a day that it did. Every other environment fails closed with exit code 5. A host with no keychain, which is most containers and servers, keeps no cache at all and is always online.

The cache is sealed against the credential that filled it, so it opens for that credential and no other. A different sign-in reads nothing out of it.

Validation happens before your command starts

A missing or wrong value stops the run with the key named, at exit code 3, instead of a crash three seconds into boot. A key the file left empty takes the default written on its schema line.

Two things penv reports on stderr and runs anyway:

  • Drift. A key that has a value and no block in the schema. penv masks it and validates nothing about it. penv check names it and tells you to add a block or drop the key.
  • A local file in cloud mode. When .env.schema names a project and a .env is still sitting there, penv uses the local file and says so on stderr.

If there is no .env.schema at all but there is a .env, run writes the schema first, using the harnesses this machine already has rather than stopping to ask. Mid-run is no place for a picker.

Masking the output

When a coding agent is driving the session, penv pipes your command's output and scrubs every sensitive value out of it as it streams. The scrubber holds back only the bytes that could still grow into a secret, so a prompt with no newline still reaches your terminal at once, and a value split across two reads is still caught.

Masking has a second way on that names nobody. When stdout is not a terminal and GIT_EDITOR is set to something that cannot prompt, such as true or cat, penv records the session as non-interactive and scrubs the output the same way. That is the only thing it changes: penv reveal and penv pull still work the way they do for a person.

Each secret is matched as raw text, as base64 in both alphabets at every offset a prefix can push it to, as upper and lower hex, percent-encoded, and JSON-escaped. What replaces it is the first two characters of the value and a run of blocks, so you can tell which value it was and do nothing with it.

The list of secrets is every value in the resolved environment except the keys your schema marks public. A value shorter than four characters is left alone, because masking it would swallow ordinary text.

--no-mask works only when stdin and stdout are both terminals and no agent is detected. Anywhere else penv ignores it and prints a line saying it did.

Do it in order

  1. penv check and fix anything it names.
  2. penv run -- <your dev command> for development.
  3. penv run --env staging -- <your command> when you need another environment.
  4. If it stops at exit 3, read the key it named and set that value.
  5. If it stops at exit 5 and you are offline, reconnect: only a cached development runs without a network.
  6. If it stops at exit 6, the environment name is refused for this identity. Check the name, or ask for a role on it.

Typed access for your language