Penv Cloud
Start free
Docs
Guidesince 1.0.0-alpha.3

Keep agents out of .env

Deny your agent .env reads, mask every penv run, seal keys with @hosts, and approve each value an agent asks to see.

Your coding agent reads what you read. penv adds four controls: harness rules that deny .env, masking in every penv run, sealed runs for keys with @hosts, and your approval before any value prints. Agent detection: coding agents.

penv guard
penv guard --check

penv guard writes what every installed harness enforces, from your schema. --check reports coverage only; penv check runs it too.

Harness rules

HarnessFileContents
Claude Code.claude/settings.jsonRead(./.env) and Read(./.env.*) denied, a sandbox denyRead block, a PreToolUse hook
Codex.codex/config.tomldeny_read over **/.env and **/.env.*; shell_environment_policy with inherit = "core"
Cursor.cursor/cli.json, .cursor/hooks.jsonDeny rules; read and shell hooks that fail closed
Amp.amp/settings.jsonAn empty guarded-files allowlist
Copilot CLI.github/copilot/permissions-config.jsonRead(**/.env) and Read(**/.env.*) denied
Gemini.gemini/settings.jsonA BeforeTool hook
Cline.clinerules/hooks/PreToolUseAn executable hook running the penv binary
Windsurf.windsurf/hooks.jsonHooks on running a command and on reading code

Since beta.2 the rules also deny penv's local key file, ~/.config/penv/local.key: with it, an encrypted .env reads as plain text. The hook refuses a penv/local.key path anywhere, %APPDATA% included.

Merging is additive, idempotent, and never weakens an existing rule. guard --check reports each file as current, stale, missing or overridden by the existing file; anything but current exits 3.

For Claude Code, guard also prints a block listing every sensitive key with "mode": "mask". Paste it into your ~/.claude/settings.json; it lives outside the repository.

Copilot CLI publishes no schema for its permissions file, so that guard is experimental and outside the 1.0 stability promise.

Native Windows runs Claude Code without a sandbox: the deny rules and hook are the only controls, and penv guard --check says so.

Hook

The installed hook is penv hook <harness>, the binary itself. A script would fail open when its interpreter is missing. Claude Code, Cursor, Gemini, Cline and Windsurf run it; Codex, Amp and Copilot CLI get rules only.

PayloadDecision
Empty stdinAllow
A shell line that runs another commandDecided again on the inner text
A path matching .env, .env.* or penv/local.keyRefuse; .env.schema is allowed by name
A glob with two or more literal characters that could reach one of those filesTreated as naming it
printenv, a bare env or set, node -e process.env, python -c os.environRefuse
penv reveal or penv pullRefuse

A .env refusal names penv ls for key names and penv check for what is missing.

Agent session behavior

Command or outputYou at a terminalAgent session
OutputAligned textOne JSON object on stdout
penv run outputMasked; --no-mask turns it offMasked; --no-mask and --no-preload ignored
Keys with @hostsValues, unless you pass --sealedPlaceholders (sealed)
penv revealPrints the valueRequests your approval, exits 4; a harness running the hook refuses it first
penv pullWrites .envExits 2; --i-am-human counts only when stdin and stdout are terminals
penv loginSigns inRefused; the fix names PENV_TOKEN
penv push --pruneDeletesRefused

Point your agent at penv run instead of pull: values reach one child process, never disk. Every cloud request from an agent session carries the harness name and session id, so our audit log names both.

Masking

Since beta.2 penv run masks on every run, your terminal and CI included. It scrubs the child's stdout and stderr as they stream, across chunk boundaries, and preloads masking into Node, Bun, Deno and Python processes.

A match becomes the value's first two characters and a run of blocks.

MaskedDetail
ValuesEvery resolved value except keys the schema marks public; drift keys (no schema block) included
EncodingsRaw; base64 in both alphabets, padded and unpadded, at every prefix offset; upper and lower hex; percent-encoded; JSON-escaped
Not maskedValues shorter than four characters; penv run and penv check name the key

--no-mask and --no-preload work only when stdin and stdout are both terminals and no agent is detected; otherwise penv ignores them and says so. Masking stops accidental leaks, not code written to disguise a value. The GIT_EDITOR trigger: environment variables.

Sealed runs

.env.schema
# @type=string(startsWith=sk_live_, minLength=32) @hosts=api.stripe.com
STRIPE_SECRET_KEY=

A key with @hosts reaches the command as a placeholder shaped by its type. A proxy inside penv run puts the real value into the request line, headers and body sent to the named hosts only; a value echoed back returns as the placeholder.

CaseResult
Agent sessionAlways sealed
You at a terminalpenv run --sealed
postgres://, redis:// URL with @hosts naming its hostLocal proxy; penv logs in with the real password
@hosts on a signing secret (HMAC, WEBHOOK_SECRET, JWT_SECRET)Refused; penv check fails it
Type with no room for a placeholderRefused before the run, cannot_seal, exit 3
Client that ignores the proxy variablesSends the placeholder and fails

@hosts is a penv addition: schema format.

Approve a reveal

Under Claude Code, Cursor, Gemini, Cline and Windsurf, the hook lets penv reveal through to your approval from 1.0.0-beta.4. In 1.0.0-beta.3 it refuses penv reveal outright, so the agent never reaches this flow.

Agent requests

penv reveal STRIPE_SECRET_KEY

penv records the key, machine, harness and session id, then exits 4 without the value:

{
  "error": "approval_required",
  "message": "STRIPE_SECRET_KEY needs a person to approve the reveal.",
  "approval": "apr_example",
  "url": "https://penv.cloud/approvals/apr_example",
  "expiresAt": "2026-09-10T12:40:00Z",
  "fix": "A person approves at https://penv.cloud/approvals/apr_example, then run penv reveal STRIPE_SECRET_KEY --approval apr_example."
}

You decide

Open the URL, check the key, device, harness and session, then approve or deny. Either answer needs secret:reveal at that environment and step-up. We expire a request after ten minutes. See approvals.

Agent redeems

penv reveal STRIPE_SECRET_KEY --approval apr_example

Prints the value once. penv checks the approval's key before spending it.

SituationResult
Same key asked twice in one sessionWe reuse the open request
UnansweredExit 4, approval_required
DeniedExit 2, approval_denied
Expired or already spentExit 4, approval_expired or approval_redeemed; ask again
Id names another keyExit 4, approval_mismatch; the id stays unspent

See the four approval codes.

A completed reveal writes four entries to our audit log: secret.reveal_requested, secret.reveal_approved (or secret.reveal_denied), secret.read, and secret.reveal naming the approver beside the session. When we refuse a redemption we add secret.reveal_refused.

Limits

In cloud mode penv guard --check states the limit: penv cannot stop a process running as you from looking. A guard is only as strong as the harness enforcing it.

Setup checklist

  1. penv guard, then commit the written files.
  2. Claude Code: paste the printed block into ~/.claude/settings.json.
  3. penv guard --check until every file is current.
  4. penv push, so no .env remains.
  5. Add @hosts to keys your agent's commands call an API with.
  6. Tell your agent to use penv run -- <command>.

GitHub Actions