This is a debug log most services have somewhere:
INFO shop-api 2.4.1 · production
DEBUG db postgres://shop:vR2x9Lq7Tz4W@prod-db/shop
DEBUG stripe sk_live_51Nq8xZ2eVbT4kP0aLm
INFO listening on :3000
It ends up in terminal scrollback, CI logs, a pasted bug report, and the context of any coding agent that ran the command. The clip below takes that app from node --env-file=.env to penv run in about 30 seconds. Everything in it runs locally, with no account.
1. Write the schema
penv init
This reads .env, writes .env.schema (key, type, required, sensitive), adds .env and .env.* to .gitignore, and writes deny rules for any coding agent it detects. In the clip that is .claude/settings.json. The schema is a plain @env-spec file, the same format varlock reads.
2. Run through penv
penv run -- node server.js
The logs are the same, but the secret values are masked. Masking is on by default in every penv run. It also works inside Node, Bun, Deno and Python through a preload, so it covers console and Response bodies, not only the terminal.
If git tracks the .env, penv run says so and prints the fix:
git rm -q --cached .env && git commit -qm "stop tracking .env"
3. Find the copies
penv scan
penv scan finds secret values that were committed to other files. In the clip it catches config/staging.env.bak:3 holding STRIPE_SECRET_KEY. Remove the value, rotate it if it was ever pushed, and read it from penv run instead.
rc.3: masking broke Next.js middleware
With mask = true, the generated env.ts and the penv run preload mask Response bodies through a subclass of Response. A NextResponse built on the original class then failed Next's instanceof Response check, and every request answered 500. rc.3 keeps instanceof Response working.
Behaviour has been frozen since 1.0.0-rc.1. Only fixes land before 1.0.0. See the stability promise.
With penv.cloud
In cloud mode, rc.2 adds checks on what penv injects:
- A key that would change how the command runs, such as
NODE_OPTIONS,LD_PRELOAD,BASH_ENV,PATHorPIP_INDEX_URL, fails the run withreserved_name(exit 3) instead of being injected. - Your command never inherits a CI runner's OIDC request token,
GITHUB_ENV/GITHUB_OUTPUT/GITHUB_PATH/GITHUB_STATE,INPUT_*orSTATE_*. -
penv loginmakes you type the code your terminal shows. A link alone can't approve someone else's terminal.
penv.cloud opens soon.
Install
curl -fsSL https://penv.cloud/install | sh
penv upgrade next # already installed
This article was drafted with AI from the penv release notes and checked against them.

Top comments (0)