EnvFolio 1.0: Keep Your Secrets Out of Plain Sight

One store, one vars file per project, and an encrypted export for machines with no store.

EnvFolio keeps your texts and secrets in one store, and hands them to a command only when you ask. Version 1.0 is out.

Quick question: where are your application secrets right now? In a .env file. In an export line in your ~/.bashrc. In your shell history, from the day you pasted a token into a curl. Plain text, in all of those, on every machine you work on.

And they travel. You work in containers, in cloud dev environments, next to AI agents that read every file in the project. Plain-text secrets go wherever your files go.

β–Ά
Rather watch? The same story is a 2Β½-minute Short: watch it here.

Meet EnvFolio

EnvFolio is a small command-line tool, folio, that keeps your texts and your secrets in one store. It hands them to a program directly, as environment variables, and only when you ask. That is the whole idea:

Not on a command line. Not in a file. Not on the screen.

With 1.0, its commands, options and file formats (the store, vars files, exports and backups) are stable: a change that breaks any of them will come with a new major version.

Built on pass

EnvFolio does not invent its own vault. It is built on pass, the standard Unix password manager. Your secrets are encrypted with your own GPG key, and every change goes into the store's git history. The store lives in ~/.folio, apart from your own ~/.password-store, but it has the same layout, so plain pass still opens every secret in it:

PASSWORD_STORE_DIR=~/.folio pass show gh/token

Texts and secrets

EnvFolio stores two kinds of items. A text, like a user name or a host, is kept plain, and signed with your key, so you can tell if someone changed it. A secret is encrypted, as it should be. The two never blur: every command that touches a secret says secret out loud, and a secret is never taken as an argument.

folio text insert -t "demo@example.com" gh/user
folio secret insert gh/token

folio ls shows both, side by side:

EnvFolio
$ folio ls
EnvFolio Store
β”œβ”€β”€ aws
β”‚   └── πŸ“„region
β”œβ”€β”€ db
β”‚   β”œβ”€β”€ πŸ”‘pass
β”‚   └── πŸ“„user
β”œβ”€β”€ gh
β”‚   β”œβ”€β”€ πŸ”‘token
β”‚   └── πŸ“„user
└── git
    └── πŸ“„user_name

Hand them to a command

To use the items, run your command through folio exec. Each item becomes an environment variable for that one command, named after its path: / becomes _, then it is uppercased. So gh/token becomes GH_TOKEN. Want another name? Put it in front: GITHUB_TOKEN=gh/token.

Texts load by default; secrets come along only with -s (--secrets). Every item is checked and read before the command starts. If one fails, the command does not run. The echo below is only there to prove the value arrived; a real command just uses it.

EnvFolio
# Use an item as a variable
$ folio exec -s gh/token -- sh -c 'echo ${GH_TOKEN}'
ghp_DEMO0000fake0000token
# Or pick its name
$ folio exec -s GITHUB_TOKEN=gh/token -- sh -c 'echo ${GITHUB_TOKEN}'
ghp_DEMO0000fake0000token

Name a folder, and every item under it comes along. Not sure what you will get? Add --names: it shows which variable gets which item, and stops. Nothing is read or decrypted, and the command does not run.

EnvFolio
# Use every items in a folder
$ folio exec -s gh -- sh -c 'env | grep GH_'
GH_TOKEN=ghp_DEMO0000fake0000token
GH_USER=demo@example.com

# Use `--names` to show which var get value from which item
# Note that `--names` will stop the command from running -- make this a dryrun.
$ folio exec --names -s gh -- gh repo list
GH_TOKEN <- πŸ”‘gh/token
GH_USER  <- πŸ“„gh/user

Need the items for a whole session instead of one command? folio shell does the same, but starts your shell. It prints the names it loaded, never their values.

Write it down once: vars files

Typing item names gets old. For a project, write down once what its command needs, and where each value comes from, in a vars file:

app.vars Β· committed with the code
GH_TOKEN=@{gh/token}
DB_URL=pg://@{db/user}:@{db/pass}@${DB_HOST}/app
LOG_LEVEL=info
@{name}a text or secret from the store
${NAME}from your environment
…:-fallbackif unset or missing: @{gh/token:-none}
@{folder/*}every item under a folder
LOG_LEVEL=infoa constant

The file holds where the values are, not the values, so commit it with the code. Nothing in it ever runs: EnvFolio reads it a character at a time and never hands it to a shell, so $(…) and backticks are refused. folio vars check checks the file without a store, in CI for example. Then one line runs the project's command. Here, deploy.sh is a stand-in that reports what it got without showing a secret:

EnvFolio
$ folio exec -s --vars app.vars -- deploy.sh
==| Super Cool App -- Deployment |==
Parameter check:
 GH_TOKEN : βœ… Given
 DB_URL   : βœ… pg://app:###@12.34.56.7/app
 LOG_LEVEL: info
Deployment will now start ...
...
--| Deploying is completed |--

Take it anywhere

Now the part that started it all: running that command on a machine that has no store, like a remote server: a CI/CD server, say. folio export packs just the items your vars file names into one encrypted file. It makes a new key for this export only, so your store's key never leaves your machine, and it always locks the file: with a passphrase you choose, or with --to, to the public key of the machine it is going to.

πŸ’» your laptop
$ folio export -s --vars app.vars -o app.folio
βœ“ Items: 3 (texts: 1, secrets: 2)
βœ“ File: app.folio

On the other side, folio exec --from runs the command with the items from that file. No store is needed, and none is made. The file is unpacked into a private temporary folder, and that folder is removed before the command starts: nothing is left behind.

☁️ a remote server, like CI/CD β€” no store
$ folio exec -s --from app.folio --vars app.vars -- deploy.sh
==| Super Cool App -- Deployment |==
Parameter check:
 GH_TOKEN : βœ… Given
 DB_URL   : βœ… pg://app:###@12.34.56.7/app
 LOG_LEVEL: info
Deployment will now start ...
...
--| Deploying is completed |--

So what may travel in the open? The vars file holds no secret, and the export file is locked: both can sit next to the code, or on the server. What opens the file is the one thing to keep to yourself: the passphrase, kept out of the repository and never next to the file; or, with --to, the secret key on the machine it is going to, which never travels at all. Your secrets get to a machine you do not own, without its provider ever touching them.

Why trust it

EnvFolio is one bash script, and it runs on the bash 3.2 that macOS ships. It needs little more than pass, gpg and git. You can read every line, and see for yourself that no secret goes where it shouldn't.

folio backup saves the whole store, its key included (still under its passphrase), into one file, and folio restore brings it back on a new machine. And if you ever stop using EnvFolio, nothing is locked in: your secrets are plain pass entries, and your texts are plain files.

Get it

Download folio from the latest release, and check it against SHA256SUMS. It is the script that sees your secrets, so mind its permissions: anyone who can change it can read them. Make it executable but writable by you only, and put it on your PATH in a folder that only you can write to:

sha256sum -c SHA256SUMS     # shasum -a 256 -c SHA256SUMS on macOS
chmod 755 folio              # everyone may run it, only you may change it
mkdir -p ~/.local/bin && mv folio ~/.local/bin/
folio version

Or with Homebrew, which checks the download against its checksum and installs it for you:

brew install nawaman/tap/folio

Then folio init makes your store, in a folder only you can get into (700), and folio help lists the rest. For Tab completion, add eval "$(folio completion bash)" to your ~/.bashrc, or the zsh one to your ~/.zshrc.

Bottom line

Your secrets don't belong in .env files, shell profiles or history. Put them in one pass store, write down once what each project needs, and let folio exec hand the values to the command that needs them, and to nothing else. When the work moves to another machine, an encrypted export goes with it, and leaves nothing behind.

Keep your secrets out of plain sight.

Links

Comments

Thank you for keeping the comment section positive, constructive and respectful. I appreciate constructive criticism & respectful disagreement!

0 / 5000