Skip to content
View penvhq's full-sized avatar

Block or report penvhq

Block user

Prevent this user from interacting with your repositories and sending you notifications. Learn more about blocking users.

You must be logged in to block users.

Content in all repositories owned by your account will be closed.
Maximum 250 characters. Please don’t include any personal information such as legal names or email addresses. Markdown is supported. This note will only be visible to you.
Report abuse

Contact GitHub support about this user’s behavior. Learn more about reporting abuse.

Report abuse
penvhq/README.md

penv

Typed .env validation, masked process output, coding-agent guards. One binary.

Install · Quick start · Schema · Agents · Deploy · Compatibility · Commands


penv validates your .env files against a committed .env.schema, starts your command with the resolved values, and masks secrets in its output. penv runs offline from your .env files; penv.cloud, our hosted service, is optional.

Install

curl -fsSL https://penv.cloud/install | sh      # macOS, Linux
irm https://penv.cloud/install.ps1 | iex        # Windows (PowerShell)
npm i -g @penvhq/cli@next                       # npm; `next` while 1.0 is in prerelease
COPY --from=ghcr.io/penvhq/penv:next /penv /usr/local/bin/penv

The installers put penv in ~/.penv/bin (%USERPROFILE%\.penv\bin on Windows) and edit no rc file. Pinning, verification and install directories: install the CLI. Channels for penv upgrade: upgrade the CLI.

Quick start

$ penv init
$ penv run -- sh -c 'echo "key=$STRIPE_SECRET_KEY port=$PORT"'
key=sk▒▒▒▒▒▒ port=3000
$ penv
version    1.0.0-beta.4
location   local
env        development
next       penv check
Values for development come from /path/to/app/.env, later files winning.

penv init writes .env.schema from your .env with a guessed type per key, adds .env and .env.* to .gitignore, and writes .penv/config.toml. Commit .env.schema. penv run validates, then starts the command.

Schema and checks

# @type=string(startsWith=sk_) @rotate=90d @docs(https://dashboard.stripe.com/apikeys)
STRIPE_SECRET_KEY=

# @type=enum(development, staging, production) @sensitive=false
APP_ENV=development
$ penv check
ok 6 key(s) in /path/to/app/.env.schema for development
rotate STRIPE_SECRET_KEY has @rotate=90d and no recorded write; penv set STRIPE_SECRET_KEY records one
note REDIS_URL is read in src/cache.ts:1 and not declared in .env.schema

The grammar is @env-spec. penv adds @assert, @rotate, @hosts and @penv: schema format. penv check also reads your source for undeclared variables; --strict fails on them. penv why KEY names where a value comes from, never the value. penv lsp serves the schema to editors: packages/vscode.

Environments

.env  →  .env.local  →  .env.<env>  →  .env.<env>.local        later file wins
--env  →  PENV_ENV  →  the key @currentEnv names  →  development

The test environment skips .env.local. A variable set in your shell overrides every file: environments and precedence.

Computed values

# @type=url @sensitive=false
DATABASE_URL=postgres://app:${DB_PASSWORD | urlencode}@${DB_HOST:-localhost}:5432/app

# @type=url @sensitive=false
API_URL=match($APP_ENV, production: https://api.example.com, _: http://localhost:4000)

# @type=string(minLength=32)
SESSION_SECRET=random(48)

penv computes these before the command starts. A value built from a sensitive key stays masked even when marked @sensitive=false; random(N) is generated once and kept in .env.local (.env.test.local for test). Functions and filters: dynamic values.

Masking and client-bundle checks

$ penv run -- npm run build
penv: dist/app.js:1 holds the value of STRIPE_SECRET_KEY, and that file ships to the browser or the app.

penv run masks each sensitive value of 4 or more characters in the command's output; only a person at a terminal can turn that off with --no-mask. A preload masks inside Node, Bun, Deno and Python processes too.

After a command under penv run exits 0, penv reads the client output folders it wrote (.next/static, dist, build, React Native bundles) and exits 3 on a secret. penv check fails a public key (NEXT_PUBLIC_, VITE_, ...) built from a secret: check.

Scanning

penv scan                   # files git would commit
penv scan --install-hook    # pre-commit hook running penv scan --staged

penv scan reports file, line and key, never the value, and exits 3 on a leak.

Encryption at rest

$ penv encrypt
$ cat .env
STRIPE_SECRET_KEY=enc:v1:…
PORT=3000

penv encrypt encrypts the sensitive values in your .env files with a per-user key kept in your OS keychain, or in a key file where there is none; every penv command decrypts where it reads. penv decrypt reverses it.

Typed access

penv gen ts        # also py, go, rust, php, java, csharp

penv gen writes a typed loader (src/env.ts, penv_env.py, env/env.go, ...) that fails on a missing or malformed value: typed env for your language.

Coding agents and sealed runs

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

penv guard writes config for Claude Code, Codex, Cursor, Copilot CLI, Gemini CLI, Cline, Windsurf and Amp. Its hooks (Claude Code, Cursor, Gemini CLI, Cline, Windsurf) call penv hook, which refuses reads of .env files, environment dumps and penv pull.

Under an agent, penv run hands a key with @hosts to the command as a placeholder, and its proxy puts the real value only into requests to those hosts.

Under a detected agent, penv refuses penv decrypt and penv bundle, refuses penv pull unless a person passes --i-am-human at a terminal, and asks you to approve penv reveal: coding agents. Agent Skill: skills/penv.

penv.cloud sync

penv login
penv push                           # sends values, deletes the files it sent; *.local stays
penv pull --env staging             # writes .env.staging

We store your team's values on penv.cloud after penv login and penv push; penv pull writes them back to a file. @penv=<provider>:org/project names another provider: docs/PROVIDERS.md.

Deploy, bundle and CI

penv bundle --env production                      # .penv/production.bundle; prints PENV_BUNDLE_KEY once
penv run --env production -- node server.js       # on the host, with PENV_BUNDLE_KEY set

penv bundle ships one environment's values encrypted with the deploy, for hosts without penv.cloud. Docker, serverless, Kubernetes and CI: deploy, packaging/docker, packaging/lambda.

Compatibility

penv check                  # in a varlock project, unchanged

penv reads a varlock schema and its .env.* files. It ignores varlock-only decorators such as @plugin with a note, and refuses exec() and plugin functions by name. penv check names the penv-only features a schema uses: @rotate, @assert, match(), random(), penv(), filters.

Against varlock 1.20.0 (penv 1.0.0-beta.2), penv run -- true takes 5.7 ms and 11 MB where the varlock standalone binary takes 234 ms and 70 MB: docs/BENCHMARKS.md. To read penv.cloud from varlock: packages/varlock-plugin.

Commands

Command Does
penv status and the next command
init create .env.schema from your .env and keep .env out of git
run run a command with your secrets loaded into it
check find problems in .env.schema and missing values
scan find secret values committed to files
ls list your keys and which ones have a value
why where a key's value comes from, never the value
set / unset save or delete one value
reveal show one value; an AI agent needs your approval first
encrypt / decrypt encrypt the secrets in your .env files, or write them back in plain text
bundle write an encrypted file of one environment's values for a deploy
gen write the typed file for your language
guard write the rules that keep AI tools out of .env
hook run as a harness hook
push / pull send your local .env to the cloud, then delete the file / write a .env file from the cloud
login / logout sign in, sign out on this machine
project, env, machine projects, environments, identities for servers and CI
schema print the schema as JSON
lsp serve .env.schema to an editor over the Language Server Protocol
upgrade replace penv with a newer release
completions print the completion script for bash, zsh, fish, powershell or elvish
help show help for a command; penv help --json prints the manifest

Output is JSON when stdout is not a terminal: global options.

Exit codes

Code Name Meaning
0 ok the command did what it says
1 error any other failure
2 auth not signed in, or the credential was rejected
3 validation the schema or the values did not pass
4 confirmation a person has to confirm; the JSON carries the replay command
5 no_credential no credential is available and none can be obtained
6 environment_refused this identity may not read that environment, or its values are write-only

Every error code: errors.

Contributing

CONTRIBUTING.md · SECURITY.md · CODE_OF_CONDUCT.md

License

MIT: LICENSE.

Popular repositories Loading

  1. penvhq penvhq Public

    Typed .env validation, masked process output, coding-agent guards.

    Rust 7

  2. penv-connect penv-connect Public

    The reusable workflow penv-cloud Connect stubs call. Public so you can read it before you run it.

    1

  3. varlock varlock Public

    Forked from dmno-dev/varlock

    AI-safe .env files: Schemas for agents, Secrets for humans.

    TypeScript