In my $HOME
, I keep a single .zshenv
file, not tracked in my
dotfiles, that exports
machine-specific tokens and API keys. Some users put environment variables like
these in .zshrc
, but I prefer .zshenv
because it’s sourced for all shell
types, including non-interactive ones like scripts or Git hooks.
It looks something like this:
source ~/.config/zsh/.zshenv
# tokens
export GITHUB_TOKEN="ghp_..."
export GITLAB_TOKEN="glpat-..."
export NPM_TOKEN="npm_..."
# API keys
export OPENAI_API_KEY="sk-proj-..."
The Problem
I’ve always felt this setup breaks several basic security practices. Imagine
accidentally running cat ~/.zshenv
during a screen share, or leaving your
terminal open in a shared space.
The Solution
I came up with a fairly simple solution that adds minimal setup and has worked reliably for over 3 months.
The solution is to use pass
to create
GPG-encrypted password files. Here’s a snippet to get started:
# Install pass
brew install pass
# You can control where password files are saved with this variable:
export PASSWORD_STORE_DIR="$XDG_DATA_HOME/pass"
# Initialise pass with a new or existing GPG key.
# This key is used to encrypt and decrypt your secrets.
# This command creates `$PASSWORD_STORE_DIR/.gpg-id` file.
pass init <GPG user ID or key ID>
# Create a password file
pass edit <pass-name>
What I like about pass
is that it follows the UNIX philosophy of using the
file system for everything. The secrets remain encrypted at rest, and are only
decrypted in-memory when accessed with pass show
. This means you can safely
put the entire password folder under version control and sync it across machines
(I do this), as long as the GPG private key is not committed or shared. Password
names are literally paths to their corresponding .gpg
files, which lets me
organise my passwords like a project directory:
$ tree ~/.password-store
.password-store
├── api
│ └── openai
│ └── cowboy-bebug.gpg
├── token
│ ├── github
│ │ └── cowboy-bebug.gpg
│ ├── gitlab
│ │ └── cowboy-bebug.gpg
│ └── npm
│ └── @your-org.gpg
└── .gpg-id
After creating passwords, the .zshenv
file can be updated like below to remove
all hardcoded tokens and keys:
source ~/.config/zsh/.zshenv
# tokens
export GITHUB_TOKEN=$(pass show token/github/cowboy-bebug)
export GITLAB_TOKEN=$(pass show token/gitlab/cowboy-bebug)
export NPM_TOKEN=$(pass show "token/npm/@your-org")
# API keys
export OPENAI_API_KEY=$(pass show api/openai/cowboy-bebug)
Extra Security
If I leave my laptop unlocked, an attacker could still run pass show
in a
terminal.
To prevent this, we can just add a passphrase for the GPG key used by pass
:
# This will trigger the GPG prompt:
gpg --edit-key YOUR_KEY_ID
# Inside gpg prompt:
gpg> passwd
# After setting/changing passphrase:
gpg> save
Now, you’ll be prompted for the passphrase each time you open a new terminal
session (or when gpg-agent
requires it). You can control how long the
passphrase stays cached by creating a gpg-agent.conf
file at
~/.gnupg/gpg-agent.conf
:
default-cache-ttl 600 # cache passphrase for 10 minutes after each use
max-cache-ttl 3600 # absolute max time (1 hour) to keep it cached
By default, GnuPG uses ~/.gnupg/gpg-agent.conf
for its agent settings. If
you’re using a custom keyring location with the GNUPGHOME
environment
variable, make sure the config file is in the correct directory.
This setup has kept my tokens secure and portable with very little friction. If you’re tired of copy-pasting secrets between machines, I highly recommend trying it.