poenv
Your configs and secrets in one encrypted vault on your machine. Switch a repo's .env between environments with one command, and let AI agents use your secrets without poenv ever showing them the values.
cd my-app poenv init # import the env file into the vault poenv use staging # the env file is now on staging poenv add OPENAI_API_KEY # pull in a key you share across apps poenv run -- npm test # inject the env without writing a file
poenv use staging rewrites the env file itself, so Next.js, Vite, Prisma, Docker Compose and Python all see the new env without wrappers.OPENAI_API_KEY once, reference it from every app, rotate it in one place.prod.Install
curl -fsSL https://poenv.pogi.codes/install.sh | shirm https://poenv.pogi.codes/install.ps1 | iexNo sudo or admin rights. Installs to ~/.local/bin (Windows: %LOCALAPPDATA%\poenv\bin), checks the SHA-256 and adds that folder to your PATH.
Installer options
install.sh flag | Env var (both installers) | Does |
|---|---|---|
--version X | POENV_VERSION | Install a specific release |
--dir PATH | POENV_INSTALL_DIR | Install somewhere else |
--no-modify-path | POENV_NO_MODIFY_PATH | Leave your PATH alone |
--yes | Don't ask anything (for agents and scripts) |
Pass flags through sh -s --:
curl -fsSL https://poenv.pogi.codes/install.sh | sh -s -- --version 0.6.1
On Windows, set the env vars before running the installer. You can also download a binary from Releases (macOS, Linux and Windows, x64 and arm64).
Update
Run the installer again. It replaces the binary and leaves your vault, PATH and repos alone. poenv --version shows what you have; --version X pins or rolls back.
Uninstall
poenv lock, so the unlocked key leaves your temp folder.- Delete the binary:
rm ~/.local/bin/poenv(Windows:%LOCALAPPDATA%\poenv\bin\poenv.exe). - Remove the line ending in
# added by poenv installerfrom your shell rc file (Windows: remove the folder from your userPath). - Optional and permanent: delete the vault in
~/.poenv(Windows:%APPDATA%\poenv). Runpoenv backupfirst if you might want your secrets back.
Repos keep working: each env file still holds the env it was last switched to. Delete .poenv and the poenv section in AGENTS.md if you like.
Quick start
-
Check your setup.
It checks PATH, the vault and the current repo, and prints the exact command for anything to fix.
poenv doctor
-
Set up a repo.
The first time on a machine,
cd your-repo poenv init
initasks you to choose a master password, then shows a 24-word recovery phrase, once. Save the phrase in your password manager: it's the only way back in if you forget the password, and it opens your backups.initthen imports your existing env file into thelocalenv, createsstagingandprod(protected), writes.poenv, gitignores it and the env file, and adds poenv notes toAGENTS.mdorCLAUDE.md. -
Fill in another env and switch to it.
Or paste
poenv set DATABASE_URL --env staging # asks for the value, hidden poenv use stagingKEY=VALUElines into the env file and runpoenv import --env staging, or runpoenvon its own to edit everything in the TUI.

Concepts
| Term | Means |
|---|---|
| Vault | One encrypted file on your machine holding every app, env and key. App names and key names are encrypted too, not only values. |
| App | One per repo, e.g. my-app. Named in the repo's .poenv file. |
| Env | A set of keys for an app: local, staging, prod, or any name you add. An env can be protected. |
| Key | A name and a value. Every change keeps the previous value: the last 10 versions per key. |
| Shared key | A key that belongs to no app, like OPENAI_API_KEY. Apps hold a live reference to it, optionally under another name. |
| Managed block | The section at the top of the env file that poenv writes. Everything outside it is yours. |
Switching envs
poenv use staging
✓ my-app: local → staging (.env.local) ~ DATABASE_URL ~ STRIPE_SECRET_KEY + SENTRY_DSN
poenv rewrites the managed block of the repo's env file and lists what changed, by key name only. With no argument, poenv use opens a picker. Running apps need a restart unless they reload .env themselves.

Manage envs
poenv env new preview --from staging # copy an env (or omit --from for an empty one) poenv env rename preview qa # the env file and .poenv follow poenv ls # apps, envs and key names
Show the env in your prompt
poenv status --short prints my-app:staging. It never opens the vault, so it's instant and safe to run on every prompt.
Protected envs
prod is protected from the start. Switching to a protected env, running with it, or copying or renaming it asks a person to type a confirmation. Agents and scripts are refused with exit 4, and --yes doesn't skip it. Toggle protection on any env with P in the TUI.
Running without a file
poenv run -- npm test # the current env poenv run staging -- npm run migrate # another env
poenv run injects the env as variables into one command and leaves the env file alone. Nothing is written to disk and no quoting applies, so use it for tools that don't read .env, for values a loader can't parse, or when you'd rather not have secrets in a file at all. It passes through the command's output and exit code. Anything after --, including --json, belongs to your command.

Frameworks & formats
poenv works with anything that reads a .env-style file. init picks the file and quoting format from the repo:
| Repo | File | Format |
|---|---|---|
Next.js (next in package.json) | .env.local | next |
Python (pyproject.toml, requirements.txt or Pipfile) | .env | python |
| Anything else: Vite, Remix, Express, Bun, Docker Compose… | .env | dotenv |
If the repo's env files already hold keys, init uses the file that has them (and asks when several do). Override with --file and --format, or edit .poenv.
The format only matters for values containing $ or escapes, which loaders read differently. Each format is checked in CI against the real Node dotenv, Next.js and python-dotenv loaders.
Vite and Bun expand $VAR in env files, so they'd read the dotenv format's unescaped $ as a variable. Use --format next there, which escapes it as \$.
A few values can't be written exactly in some formats (for example ${…} for python). poenv writes them as best it can, lists the keys, and suggests poenv run.
Java & Spring Boot
Spring Boot doesn't read .env files, so poenv use alone changes nothing for it. Inject the env with poenv run:
poenv run -- ./mvnw spring-boot:run poenv run staging -- ./gradlew bootRun
Spring reads environment variables natively: reference them in application.properties (spring.datasource.password=${DB_PASSWORD}), or name keys for relaxed binding (SPRING_DATASOURCE_URL sets spring.datasource.url).
With Docker Compose, env_file: .env reads the file poenv writes. Avoid spring.config.import=optional:file:.env[.properties]: properties files don't strip quotes, so any value poenv quotes keeps its quotes.
The TUI
Run poenv on its own in a terminal to browse and edit apps, envs and keys. Values stay masked until you press v. Inside a repo it opens on that repo's app. Lowercase keys act on the selected key, uppercase on the whole env.

| Key actions | |
|---|---|
| a | Add a key |
| r | Add a reference to a shared key (optionally renamed) |
| e / ⏎ | Edit the value |
| d | Delete the key |
| s | Move the key into the shared pool (keeps its history) |
| v | Show / hide the value |
| Env actions | |
| u | Use this env: writes the env file, like poenv use |
| A | New env |
| R | Rename env |
| P | Toggle protected |
| D | Delete env |
| Navigation | |
| ← → / tab | Previous / next env |
| ↑ ↓ | Previous / next key (vim's hjkl work too) |
| esc / backspace | Back to the app list |
| i | Set up this repo (shown when it isn't yet) |
| q | Quit |
Locking & CI
poenv stays unlocked for 12 hours at a time, across every terminal on the machine. When it's locked, commands and the TUI ask for your password.
poenv unlock --for 8h # any duration: 30m, 8h, 2d poenv lock # close it early poenv status # shows the lock state
Agents can't type the password, so while locked they get exit 5 and ask you to run poenv unlock.
On CI and servers, pipe the password in from your secret store:
printf '%s' "$VAULT_PASSWORD" | poenv unlock --stdin
Set POENV_HOME to keep the vault somewhere other than the default folder.
Backup & recovery
poenv backup backup.enc # encrypted copy of the vault poenv restore backup.enc # restore it, on this machine or another
A backup plus your recovery phrase restores everything on any machine: on a new machine, restore asks for the phrase, then for a password for that machine. Restoring over an existing vault asks first and keeps the old one as store.enc.bak-<time>.
| Situation | Run |
|---|---|
| Forgot the master password | poenv recover: type the phrase, choose a new password |
| Check the phrase you saved is right | poenv recover --check |
| Phrase may have leaked | poenv recover --new-phrase (older backups still need the old phrase) |
| Change the password | poenv passwd |
| Undo a bad change to a key | poenv history KEY, then poenv rollback KEY [N] |
Lose both the password and the phrase, and the vault can't be opened. Nobody can reset it for you.
Commands
Every command takes --json for machine-readable output, except poenv run. Values are never accepted as arguments: set asks for them hidden, or reads them from stdin.
Daily
| Command | Does |
|---|---|
poenv | Open the TUI |
poenv use [ENV] | Switch this repo's env file to ENV (picker if omitted) |
poenv add KEY [--as NAME] [--from APP/ENV] [--env ENV] | Add a shared key (live reference) or copy one from another app |
poenv status [--short] | Current app, env and lock state (--short → my-app:staging) |
poenv run [ENV] -- CMD | Run CMD with ENV injected, without touching the file |
Setup & management
| Command | Does |
|---|---|
poenv init [--app NAME] [--env ENV] [--file FILE] [--format F] [--no-import] [--yes] | Set up a repo: import the env file, write .poenv, gitignore, agent notes |
poenv set KEY [--shared] [--env ENV] [--empty] | Set a value (asked for hidden, or piped in). Empty input is an error unless --empty |
poenv import [--env ENV] | Save keys you pasted into the env file to the current env (or ENV), overwriting existing ones |
poenv rm KEY [--shared [--keep-copies | --everywhere]] [--env ENV] | Remove a key (see shared keys) |
poenv env new NAME [--from ENV] | Add an env, empty or a copy of ENV |
poenv env rename OLD NEW | Rename an env; the env file and .poenv follow |
poenv file [NAME] | Show or change the env file poenv manages; the block moves with it |
poenv ls [APP] | Apps, envs and key names (never values) |
poenv unlock [--for 8h] [--stdin] | Unlock with your password (12h by default) |
poenv lock | Lock now |
poenv doctor [--fix] | Check your setup; --fix applies the safe fixes |
Just for you
These reveal values or touch the whole vault, so they're refused for agents and scripts (exit 4).
| Command | Does |
|---|---|
poenv get KEY [--shared] [--env ENV] | Print a value |
poenv history KEY [--show] | Previous values (last 10, masked unless --show) |
poenv rollback KEY [N] | Restore a previous value |
poenv backup [FILE] [--force] | Encrypted backup |
poenv restore FILE | Restore a backup |
poenv passwd | Change your master password |
poenv recover [--check | --new-phrase] | Use your phrase / check it / make a new one |
history, rollback and rm also take --shared and --env ENV.
Exit codes
Stable, so scripts and agents can act on them. Errors always say the command that fixes them.
| Code | Means | What to do |
|---|---|---|
0 | OK | |
1 | Error | Read the message; try poenv doctor |
2 | Not set up (no .poenv or no vault) | poenv init |
3 | Unknown app, env or key | poenv ls to see what exists |
4 | Refused: human-only command or protected env | A person runs it in their own terminal |
5 | Waiting on the user (locked, or the vault needs creating) | A person runs poenv unlock or poenv init |
The env file
poenv owns one block at the top of the env file and never touches anything else (except during init and import, which move the keys they import into the block):
# >>> poenv >>> (managed: edits here are overwritten)
# app: my-app env: staging
DATABASE_URL=postgres://staging-db/app
OPENAI_API_KEY='sk-…'
# <<< poenv <<<
DEBUG=true # yours: kept as-is, and it wins over the blockBecause the block is at the top, your own lines below it override it in every loader. A key set in both places is reported as a conflict.
A change made anywhere (set, add, rm, rollback, or the TUI) also rewrites the block in every other repo on this machine whose current env it affects. Each repo stays on its own env.
The .poenv file
Written by init at the repo root. It holds names only, never secrets.
app = "my-app" env = "local" # the env the file is on file = ".env.local" # the env file poenv writes format = "next" # dotenv | next | python
It's gitignored by default. You can commit it instead: a teammate's poenv init in a fresh clone then adopts its settings, and poenv leaves a tracked .poenv alone.
Files & settings
| Path | Holds |
|---|---|
~/.poenv/store.encWindows: %APPDATA%\poenv\store.enc | The vault, encrypted as a single blob |
keys.json (next to the vault) | The master key, encrypted with your password |
repos.json (next to the vault) | Paths of repos that use poenv, so changes reach them. No values; not in backups |
| Per-user temp folder | The unlock session (owner-only), kept out of the vault folder so backups and sync never copy it |
| Env var | Does |
|---|---|
POENV_HOME | Folder for the vault and its files |
POENV_DEBUG | Print stack traces on errors |
For AI agents
Agents can do the daily work: check status, list key names, switch envs and run commands with secrets injected. poenv never prints a value to them, never accepts one as an argument, and keeps values out of --json.
poenv status --json # which app/env this repo is on poenv ls --json # envs and key names, never values poenv use staging --json # switch the env file poenv run staging -- npm test # run with an env injected poenv doctor --json # when something is off: follow each "fix"
Agents are refused (exit 4) for get, history, rollback, backup, restore, recover and protected envs, and can't create the vault (exit 5), since that shows the recovery phrase.

poenv init adds a short section to AGENTS.md / CLAUDE.md telling agents these rules. To have an agent install poenv, point it at llms.txt or the agent install guide, or say:
Install poenv (https://poenv.pogi.codes/llms.txt) and set up this repo.The /poenv skill
Install the poenv agent skill once and every coding agent on your machine knows these rules in any repo: Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and more.
npx skills add pogicodes/poenvThen type /poenv setup or /poenv use staging ($poenv in Codex). Agents also reach for it on their own when you ask about env vars.
How poenv tells people from agents
A person has an interactive terminal on stdin and stdout and none of the markers coding agents set (CLAUDECODE, CODEX_SANDBOX, CURSOR_AGENT, GEMINI_CLI, AI_AGENT, …). A CI variable counts as a script too.
Security model
- While locked, nothing can read your secrets: not other programs, not agents. The master key is stored only encrypted with your password (scrypt).
- While unlocked, the master key sits in an owner-only session file until the session expires. Programs running as you can read it then, which is what lets agents work.
poenv lockends it early. - Encryption protects the vault file against being copied: backups, sync folders, a stolen disk. It is not a sandbox against programs running as you.
- Values are never printed by agent-usable commands, never accepted as arguments (so they stay out of shell history), and never in
--jsonoutput. - Once a value is written to an env file, anything that can read that file can read it, and a command run with secrets injected can print them.
poenv runavoids writing files at all.
These rules keep secrets out of poenv's own output. They are not a sandbox: an agent that can read your files can read a written .env.
Troubleshooting
Start with poenv doctor. It checks PATH, the vault, the lock, your saved phrase, gitignore, agent notes, dangling shared references and whether the env file is up to date, and names the fix for each. poenv doctor --fix applies the safe ones; it never runs sudo.
poenv: command not found after installing
Open a new terminal so the PATH change applies, or use the full path the installer printed.
My app still sees the old values
Restart it: most dev servers read .env only at startup. Also check you don't set the same key below the poenv block, since your lines win.
A value with $ comes out wrong
Your loader expands variables. Switch the format (--format next escapes $), or use poenv run. See Frameworks & formats.
An agent keeps getting exit 5
poenv is locked, or the vault doesn't exist yet. Run poenv unlock (or the first poenv init) in your own terminal. Never give the agent your password.
