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 devThe 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
| Mode | How penv decides | Roughly |
|---|---|---|
| Local | Parse .env, validate, start the command | Under a millisecond to find the schema |
| Cloud, warm cache | Open the encrypted cache with the keychain key | About 2ms |
| Cloud, revalidating | HEAD the environment with the ETag it holds | About 40ms |
| Cloud, changed | GET the environment and rewrite the cache | One 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 checknames it and tells you to add a block or drop the key. - A local file in cloud mode. When
.env.schemanames a project and a.envis 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
penv checkand fix anything it names.penv run -- <your dev command>fordevelopment.penv run --env staging -- <your command>when you need another environment.- If it stops at exit 3, read the key it named and set that value.
- If it stops at exit 5 and you are offline, reconnect: only a cached
developmentruns without a network. - If it stops at exit 6, the environment name is refused for this identity. Check the name, or ask for a role on it.