Keeping secrets out of your shell environment
Over the years, every developer accumulates a collection of credential files. ~/.aws/credentials stores your AWS access keys. %APPDATA%\Roaming\NuGet\NuGet.Config holds your package feed tokens.
Various .env files scattered across project directories contain database passwords and API keys. Some of those end up in shell startup files as export statements. It all works, until it doesn’t.
The problem with this approach isn’t that the secrets exist — it’s where they live and for how long. Credential files sit on disk permanently, readable by any process that runs as you. Shell environment variables are inherited by every subprocess you spawn, whether you intended that or not. That includes every postinstall script that fires when you run npm install, every build tool plugin, every pre-commit hook. The secret you exported for one purpose is now ambient: available to everything, for as long as your shell session lasts.
The supply chain angle
A compromised or malicious package doesn’t need to be clever about exfiltrating your secrets. A few lines in a postinstall script can read ~/.aws/credentials directly, or enumerate the process environment and POST anything that looks like a key to a remote server. It happens silently and you’d have no way of knowing until your AWS bill arrives.
This isn’t hypothetical. Supply chain attacks against npm, PyPI and other package ecosystems have been documented repeatedly over the last few years, and credential theft is a common goal.
The fix isn’t to stop using package managers. It’s to stop leaving your credentials lying around where they can be found.
How Envoke works
Envoke (ee) is a small command-line tool that injects secrets into a single subprocess, on demand. Secrets are fetched from one or more backends (more on those shortly), injected into the child process environment, and discarded when the process exits. They never exist in your shell, they never touch disk, and they’re never available to anything other than the command you explicitly invoked.
The idea is to replace your credential files entirely. Delete ~/.aws/credentials. Instead:
ee aws s3 ls s3://my-bucket
Envoke fetches your AWS credentials from its backend, spawns aws s3 ls with those credentials in its environment, and they’re gone when the command finishes. The credentials were never in your shell and were never on disk.
The same pattern works for anything that reads from the environment:
ee make dev
ee psql -h $DB_HOST -U $DB_USER
ee dotnet restore
Getting started
For Windows, download a pre-built binary from the releases page for your platform. Linux and Mac users can also use Homebrew to install using brew install gerco/tap/envoke.
The released binaries include the OS keychain and AWS Secrets Manager backends.
Store your first secret:
ee set aws-test AWS_ACCESS_KEY_ID
ee set aws-test AWS_SECRET_ACCESS_KEY
This stores the values in your OS keychain (macOS Keychain, Windows Credential Manager, or the system keyring on Linux), under the namespace aws-test. The values are never written to any Envoke config file.
Then run a command with those secrets injected:
ee aws-test -- printenv | grep AWS
The .envoke file
For projects where the set of required secrets is known and stable, you can commit a .envoke file to the repository. This file declares which namespaces a project needs and where to source them from:
namespaces:
- name: aws-test
- name: my-project-db
backend: aws
When you run ee -- <command> from a directory containing this file, all declared namespaces are injected automatically. Teammates clone the repo, configure their own backends, and run the same command without needing to coordinate on secret names or manually export anything.
The .envoke file contains only namespace names and backend references — never secret values. It’s safe to commit. Secret values stay in the backend.
For personal overrides, an .envoke.local file (gitignored) follows the same format and is merged on top. This is useful when you want to point a namespace at a local or staging backend without affecting what’s committed.
Backend credentials (the configuration Envoke itself needs to talk to AWS Secrets Manager, for example) live in a global config file that is never committed. They can also be injected by a previous namespace, like demonstrated above. The AWS credentials are loaded from aws-test in the OS keychain (the default backend) and those credentials are then used to retrieve the my-project-db secret from AWS Secrets Manager.
Backends
The current release ships with two mature backends:
OS Keychain — Always available. Uses the native credential store for your platform: Keychain on macOS, Credential Manager on Windows, and the system keyring on Linux. Suitable for personal secrets and local development.
AWS Secrets Manager — Retrieves secrets from AWS. Useful for shared team secrets or production-adjacent workflows where you want a single source of truth that isn’t a file on someone’s laptop.
Support for 1Password Secrets Manager, Keeper and JumpCloud Password Manager is on the roadmap but not yet production-ready and not included in the published binaries.
Caveats
Envoke wraps your command, so it needs to be present at every invocation. That’s a habit change. If you’re used to exporting credentials once and forgetting about them, you’ll need to adjust. It’s possible, but discouraged to use ee bash.
ee status is useful when something isn’t working — it checks connectivity and authentication for all configured backends and reports which namespaces are healthy.
Secrets are injected into the process Envoke spawns. Grandchild processes inherit that environment normally, which is usually what you want. But if a process detaches from the subprocess tree and later tries to use the credentials, they won’t be there — by design.
Lastly: Envoke protects your secrets from processes you run. It doesn’t protect against a compromised system, a malicious binary you executed directly, or anything running as root. It’s one layer of a defence-in-depth approach, not a complete solution on its own.
Source
The source is on GitHub at gerco/Envoke. Issues and pull requests welcome.