skip to content

Search

Securely Exporting API Keys as Environment Variables

3 min read

Learn how to securely manage API tokens in your shell using pass, replacing hardcoded .zshenv values with encrypted lookups.

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.