Run a command with your secrets and settings as environment variables, kept in a
pass store — the way op run and aws-vault exec do, with
nothing but bash, pass and gpg.
folio exec --secrets gh/token -- gh release list # gh gets GH_TOKEN- Two kinds of item. A secret is encrypted, as in
pass. A text is a plain value — a URL, a user name, a region — kept readable but signed, so a changed one is caught. "Item" means either. - Built on
pass. The store is apassstore with a git history;passitself can still read every secret in it. - Easy to move. One command each to back up, restore, export and import items.
- Made for places you don't fully trust. Export only what a cloud machine needs, locked to
its key, and run there with no store at all. One motivation is a cloud CodingBooth, where users
can put their sensitive items for themselves to use inside the booth, in a way that
CodingBooths.online(the provider) never needs to touch the actual secrets.
Videos and articles: nawaman.github.io/EnvFolio · Install: below · Every command, with examples: MANUAL.md · What changed: CHANGELOG.md · Download: the latest release
With Homebrew (macOS or Linux):
brew install nawaman/tap/folioWithout Homebrew, with the install script:
curl -fsSL https://raw.githubusercontent.com/NawaMan/EnvFolio/main/install.sh | bashIt downloads folio from the latest release, checks it against the release's SHA256SUMS, and
puts it in ~/.local/bin — no sudo. | FOLIO_INSTALL_DIR=<folder> bash puts it elsewhere,
| FOLIO_VERSION=1.0.1 bash installs that release. Read
install.sh first if you'd rather. Run it again to upgrade.
Or by hand — EnvFolio is one bash script. Download folio and SHA256SUMS from the
latest release, check it, make it
executable and put it on your PATH:
cd "$(mktemp -d)"
curl -fsSLO https://github.com/NawaMan/EnvFolio/releases/latest/download/folio
curl -fsSLO https://github.com/NawaMan/EnvFolio/releases/latest/download/SHA256SUMS
sha256sum -c SHA256SUMS # macOS: shasum -a 256 -c SHA256SUMS
chmod +x folio && mkdir -p ~/.local/bin && mv folio ~/.local/bin/
folio versionIf folio version is not found, ~/.local/bin is not on your PATH (often the case on macOS):
add export PATH="$HOME/.local/bin:$PATH" to your shell's rc file, or move folio to another
folder on it. To upgrade, do the same with the new release; to uninstall, delete the file
(either way you installed it).
For Tab completion of commands, options and item names, add eval "$(folio completion bash)" to
~/.bashrc, or eval "$(folio completion zsh)" to ~/.zshrc. Homebrew installs it for you, for
shells that load completion from Homebrew's folders (see
folio completion).
bash3.2 or newer (the one macOS ships is enough)pass,gpg,git,tree,tar,gzip- one clipboard tool, only for
-c:pbcopy(macOS),wl-copy(Wayland) orxclip(X11) - optionally, a GUI pinentry to have gpg ask for passphrases in a window — see Passphrase prompts in a window
brew install pass gnupg tree # macOS
sudo apt install pass gnupg git tree # Debian / Ubuntugpg 2.4 is fine. 2.5.21 or later can also make a post-quantum key — see
Updating gpg for post-quantum keys. On Windows, EnvFolio
runs in WSL.
folio init # make the store, and a GPG key for it if you want one
folio insert secret gh/token # asks for the secret, never shows it
folio insert text -t eu-west-1 aws/region # a text can be given on the command line
folio ls # both, as a tree
folio exec --secrets gh aws -- ./release.sh # gets GH_TOKEN and AWS_REGION
folio shell --secrets gh # a shell with GH_TOKEN; `exit` to leaveTexts load by default; a secret loads only with --secrets. Use --names to see which variable
gets what without reading any value:
folio exec --secrets --names gh aws -- ./release.sh| Secret | Text | |
|---|---|---|
| Stored as | <name>.gpg, encrypted to the store's key |
<name>.txt, plain, plus a signature <name>.txt.sig |
| Reading it | needs the key's passphrase | needs no passphrase; refused if changed outside EnvFolio |
exec / shell |
only with --secrets |
loaded by default |
show -c |
clipboard cleared after 45 seconds | clipboard not cleared |
| For | tokens, passwords, private keys | URLs, user names, regions, ports — anything not secret that must not be tampered with |
A text and a secret can share a name. The commands always say which kind they mean:
folio show secret web/github, folio show text web/github — so a secret is never shown by
accident.
| Command | What it does |
|---|---|
folio init |
Create the store (and its GPG key, or use one you have) |
folio path |
Print the store's folder |
folio backup / restore |
The whole store and its key, in one file and back |
folio export / import |
Some items, in one locked file and into another store |
folio ls [text|secret] |
List items, as a tree or flat — of the store, or with --from, of an export file |
folio info <name> |
What is known about one item, never its value: size, variable, created/modified (from git), its signature or the keys it is encrypted to |
folio <action> text|secret |
show, insert, edit, generate, rm, mv, cp, find (by name), grep (by value) |
folio exec |
Run a command with items as environment variables |
folio shell |
Start a shell with items as environment variables |
folio check vars |
Check how vars files are written |
folio completion bash|zsh |
Print the Tab completion script |
folio help [command], folio version |
Help and version |
The kind can come first too: folio secret show is folio show secret. Every command takes
--fresh-passphrase to have gpg-agent forget the store key's passphrase when it starts and ends,
and -q (--quiet) to leave out the notes on what happens next: only the output, on stdout (the
notes go to stderr either way).
Details: MANUAL.md.
An item's variable name comes from its path:
- a top folder whose name starts with
@— a namespace — is dropped, or with--use, the namespaces it names; /,-and.turn into_;- it is uppercased.
| Item | Variable |
|---|---|
gh/token |
GH_TOKEN |
@me/gh/token |
GH_TOKEN |
gh/api-key |
GH_API_KEY |
gh/api+key |
none — not a valid name; left out with a warning |
Namespaces can nest from the front of the path — @app1/@prod/db/user — but naming drops only the
top one: items under a nested namespace are left out, with a note to add --use @app1/@prod,
which peels the whole prefix (see One contract, many environments).
After a folder without @, @ is an ordinary character: gh/@work/token is not a valid name.
To pick the name yourself, write VAR=<name> on the command line:
folio exec --secrets GITHUB_TOKEN=gh/token -- gh release listA vars file says what a command needs, by its own names, and where each value comes from —
@{name} is an item, ${NAME} a variable already set:
# deploy.vars
GITHUB_TOKEN=@{gh/token} # gh/token, as GITHUB_TOKEN
AUTH=Bearer @{gh/token} # put into a longer value
REGION=@{aws/region:-${AWS_REGION}} # with a fallback when the item is not there
@{aws/*} # every item under aws/, each named by its pathfolio check vars deploy.vars # check how it is written (no store needed)
folio exec --secrets --names --vars deploy.vars # show where each variable would come from
folio exec --secrets --vars deploy.vars -- ./deploy.shWith --vars, the command gets only what the file sets. A secret still needs --secrets. See
From a vars file.
Experimental:
--usemay change in a later version.
Keep each environment's items under its own namespace, and pick one at run time with --use. It
takes every item under the namespace as if the namespace were not in its path:
server/port (T) 22
@app2/@prod/server/{db, user} (T)
@app2/@prod/server/password (S)
@app2/@staging/server/...
# server.vars — the contract, written once for the app
DB_USER=@{server/user}
PGPASSWORD=${SERVER_PASSWORD}
DB_URL=postgres://${SERVER_USER}@${SERVER_DB}:${SERVER_PORT:-5432}/appfolio exec --secrets --vars server.vars server --use @app2/@prod -- ./deploy.shserver and --use @app2/@prod load server/port and prod's server/db, server/user and
server/password as SERVER_PORT, SERVER_DB, SERVER_USER and SERVER_PASSWORD, and the vars
file builds what the command gets from them. Where two give the same item, the one typed last
wins. Or export one file per environment and run where there is no store:
folio export --secrets server --use @app2/@prod -o app2-prod.folio # here
folio exec --from app2-prod.folio --secrets --vars server.vars --all -- ./deploy.sh # thereSee Feeding a command.
What EnvFolio does:
- Secrets never go on a command line, into a file, or on the screen unless you ask to see
one. They are read at a hidden prompt or from a pipe and handed to
passon stdin; there is no option that takes a secret as an argument. execandshellare the one exception, on purpose: with--secrets, they put secrets into the command's environment — that is their job. Anything running as you can read a process's environment (/proc/<pid>/environon Linux), so run only commands you trust.--debugand--confirmshow what a run will load before any value is read.- Texts are signed. A text changed, or signed by another key, outside EnvFolio is refused, and the message says how to see what changed.
- The store folder is private (
700). EnvFolio warns whenever it belongs to someone else or other users can get into it. - The clipboard is cleared after 45 seconds for a secret (
PASSWORD_STORE_CLIP_TIME). - An export is always locked, with a passphrase or to a public key (
--to), and carries a key made for it alone — your store's key never leaves your machine. That key can have a passphrase of its own too, if you say so when you export, and be post-quantum (--key-type pqc; classical by default, so any gpg opens the file). - A backup holds the store's secret key (still protected by its passphrase), the texts and the
names of the secrets.
--encryptlocks the whole file with a passphrase of its own. Keep it somewhere safe and offline.
What it does not do: protect against someone who already runs code as you, or as root. The secrets are as safe as your GPG key and its passphrase.
The store is a git repository, and every change is committed. EnvFolio has no push or pull,
but you can add a remote yourself — a private one: the texts and the names of the secrets are
plain in it, and the secrets are encrypted to your key.
On the first machine, back up with the history, and push:
folio backup --history /media/usb
cd "$(folio path)"
git remote add origin git@example.com:me/folio-store.git
git push -u origin HEADOn another machine, restore that backup (it brings the key and the same history), then pull and push as usual:
folio restore /media/usb/backup-…--folio.tar.gz
cd "$(folio path)"
git remote add origin git@example.com:me/folio-store.git
git fetch origin && git branch -u "origin/$(git branch --show-current)"
git pullWithout --history, a restore starts a new history that cannot be pulled into.
folio backup and folio restore alone, or folio export and import, also move items
between machines without a remote.
The store is kept apart from your own ~/.password-store. Point pass at it to read a secret:
PASSWORD_STORE_DIR="$(folio path)" pass show gh/tokenTexts are not pass entries: they are the .txt files next to the secrets.
FOLIO_STORE_DIR— the store's folder. Defaults to$HOME/.folio.FOLIO_FILE_DIR— a folder of.folioexport files, so one is named without its path:--from app2-prodis$FOLIO_FILE_DIR/app2-prod.folio, forexec,shell,show,ls,importandexport -o. A name ending in.foliois always a path. See An export's path, or its name.FOLIO_STORE_PASSPHRASE_FILE— a file of yours only (chmod 600) whose first line is the store key's passphrase, so it is never asked — for a machine you trust. See Never asked, on a machine you trust.FOLIO_FRESH_PASSPHRASE— set to1to have gpg-agent forget the store key's passphrase when each command starts and ends, as--fresh-passphrasedoes. See Leave no passphrase behind.FOLIO_QUIET— set to1to leave out the notes on what happens next (the picture of an export's locks,Copied …), as-qdoes. See Just the output.FOLIO_SHELL— set to1insidefolio shell, for your prompt to show.pass's own settings apply too, such asPASSWORD_STORE_CLIP_TIME(how long a copied secret stays) andPASSWORD_STORE_GENERATED_LENGTH(the length ofgenerate).
- Follow what
passdoes as much as possible. - Users of
passshould be able to get to their secrets usingpassdirectly. - It must be clear when secrets are involved. The CLI uses the word "secret" explicitly, to avoid exposing secrets by accident.
- Simplify things when possible (opinionated).
- Explain clearly so there is no confusion.
- Plain, linear bash, so anyone can read it and see that no secret is sent anywhere.
passalone — EnvFolio is apassstore; it adds texts, running commands with items as variables, vars files, namespaces, and backup/export in one file.op run,aws-vault exec— the same idea, without an account or a service: the store is files and a GPG key on your machine..envfiles,direnv— those keep values in plain files next to the code. With EnvFolio, a vars file holds the names and the store holds the values.sops— encrypts whole config files for a team in a repository. EnvFolio is a personal store that hands values to commands.
- The store key's expiry is not watched: once it expires, adding or changing items fails, and there is no command yet to extend or rotate the key.
- No
push/pullof its own — see Syncing the store with git. - Windows only through WSL.
--useis experimental.
EnvFolio is developed in a CodingBooth container that has every tool:
just test-all # every test, under bash 3.2 and bash 5
./booth exec --run -- shellcheck folio install.sh tools/*.sh tests/test-all.shRead AGENTS.md for the conventions — bash 3.2, Linux and macOS, how secrets are handled, and how tests stay away from your real store.
A post-quantum key needs gpg 2.5.21 or later on every machine that opens the store, a backup of
it, or an export locked to it. Check with gpg --version. If it is older, EnvFolio keeps working
with classical keys, but it cannot make, restore or open post-quantum ones.
- macOS — install gpg with Homebrew:
brew install gnupg(orbrew upgrade gnupg). Ifgpg --versionstill shows an older one (for example GPG Suite's), put Homebrew'sbinfolder ahead of it on yourPATH. - Linux — if your distribution's package is already 2.5.21 or later, upgrading it is enough.
Many still ship 2.4; then install gpg with Homebrew on Linux
(
brew install gnupg) and put itsbinfolder ahead on yourPATH, or build it from gnupg.org. gpg 2.5.23 and 2.5.24 as released have a bug (T8469): they cannot use an ECDH key (such as gpg's default, cv25519) imported without a passphrase. Homebrew's has the fix; building it yourself, add the two commits linked from that bug, or take a later 2.5. Ubuntu's 2.4.8 (with libgcrypt 1.12.0) has a bug of its own that looks alike: it often cannot use a cv25519 key it has just imported, with a passphrase or without. An export made with EnvFolio 1.1.0 or later has an RSA key, which both can use; one made before has a cv25519 key — export it again, or open it with Homebrew's gpg.folio restoreimports the store's own key into your keyring: if that key is cv25519, as gpg makes by default, use Homebrew's gpg there. - Windows — EnvFolio runs in WSL: follow the Linux steps there.
Then stop the old gpg-agent, so the new gpg starts its own: gpgconf --kill all. pass uses a
command named gpg2 instead of gpg when there is one: if gpg2 --version shows an older gpg,
remove it or put the new one first.