Run claude, codex, cursor, amp, whatever. It starts a shell as your user. Your user can read .env. So the agent can read .env.
That is the whole problem. Not a bug in any harness, not a jailbreak, just the permission model working as designed. And it means that whenever an agent greps around your repo for context, or a tool call echoes the environment, or a stack trace lands in a transcript, your Stripe key can go with it.
Most harnesses have a deny list you can configure. Almost nobody does, the syntax differs per tool, and a deny rule on .env does nothing about .env.local, .env.production, or the fact that printenv in a hook still works.
We built penv because we were tired of hand-writing those configs and then not trusting them.
What penv actually does
One static Rust binary. No runtime, no daemon, no account required for the first minute.
curl -fsSL https://penv.cloud/install | sh # or: npm i -g @penvhq/cli
penv init # reads .env, writes .env.schema, gitignores .env
penv run -- pnpm dev # validates, injects, masks
init writes exactly one file into your repo, .env.schema. It looks like this:
# @schema=1
# @type=url
DATABASE_URL=
# @type=string
STRIPE_SECRET_KEY=
# @type=port @sensitive=false
PORT=3000
No values that could be secrets are ever copied in. Every key is sensitive and required by default; a bundler prefix like NEXT_PUBLIC_ or an obviously dull value like 3000 flips that, and a key named like a secret (STRIPE_SECRET_KEY, ..._ANON_KEY) stays sensitive no matter what it holds. The decorators follow the @env-spec vocabulary, so if you have used varlock you can already read it.
The schema is committed. .env is not. That split is what makes the rest possible.
What that gives you against an agent
Validation before exec. penv run checks every value against the schema before your process starts. A missing or malformed key fails with exit code 3, not a runtime error forty seconds in.
Masking. When penv detects an agent session (it reads the harness env vars first, then walks process ancestry), it scrubs every sensitive value from the child's stdout and stderr — raw, hex, base64, and URL-encoded forms. The agent sees ***, not the key.
Harness guards, generated. penv guard writes what each harness actually enforces, from the schema:
- deny rules and a sandbox block for Claude Code
- a permission profile for Codex
- deny rules and fail-closed hooks for Cursor
- the equivalents for Copilot CLI, Gemini, Cline, Windsurf, and Amp
The hook is the penv binary itself, never a shell script that fails open when something goes wrong.
Typed access. penv gen ts or penv gen py writes a typed accessor for your language so code stops doing process.env.FOO! and reads through a validated object. Targets are folders with a template; adding a language is adding a folder.
The claim, and only the claim
penv guard --check prints one sentence, and we keep it exactly true:
penv keeps secrets out of the files, the repo, the shell history and the captured output an agent reads.
That is the layer where leaks actually happen — a grep, a transcript, a pasted stack trace — and it is the layer penv closes. penv is not a sandbox and does not claim to be one; the process you launch still runs as you. So the cloud side is built for that: every value it issues is short-lived, scoped to the session, and attributable to it. A value that escapes is already expired and already traced.
The cloud is the upgrade, not the product
Everything above works with zero account. When you have a team:
penv login # device code in the browser
penv push # values go to penv.cloud, .env is deleted
From there .env is a view you can regenerate, not the source of truth. A teammate clones and runs penv run -- pnpm dev; that is the onboarding. CI presents its OIDC token and gets a fifteen-minute credential. A server with nothing to present enrols a keypair once. penv reveal KEY under an agent session needs a human to click approve in the console — the agent can ask, it cannot self-serve.
Status, honestly
@penvhq/cli is at 1.0.0-alpha.3. It is published, signed, builds for macOS/Linux/Windows on x86_64 and arm64, and we use it daily. We do not expect further breaking changes before RC, but it is an alpha. Local mode needs nothing from us.
penv.cloud is in private beta. We are letting early users in from the waitlist in small batches, and the teams we onboard during the beta keep enterprise-tier benefits at launch — if you want your team on it, sign up.
If you run a coding agent against a repo with a .env in it — and you almost certainly do — try penv init on it and read the schema it writes. If it guesses a key wrong, that is a bug report we want.
- Source: https://github.com/Itzfeminisce/penvhq
- Install + waitlist: https://penv.cloud
- npm: https://www.npmjs.com/package/@penvhq/cli
MIT.
Top comments (2)
love to see the @env-spec adoption!
Thanks Phil — the spec did the hard part.
@env-specgave us a vocabulary people already read, so we only had to add the bits penv needs (@since,@rotate,@dynamicFrom, and the@penv=header to name the cloud project).