Nicolas Noben

Store env variables in macOS keychain for added security

productivity, security, safety, env keys

2026-10-30

Introduction

In world where stolen credentials by supply chain attack and models running through your system, storing API keys and secrets in .env or in ~/.config is trivially accessible and dangerous.

I provide a way to store your env vars/keys in keychain on macOS and potentially GNOME Keyring or KWallet on Linux via secret-tool.

Introducing wkey

wkey stands for with-key and you can basically name it what you want.

The idea is to have a couple of scripts that we place in our paths to set, read and delete keys; the reading will require a password meaning applications running in your suid cannot read the env keys without a password prompt.

To run a program that requires an env key, once set, you'd run the program like so

wkey MY_API_KEY -- python program.app (for example).

To write a key to the macOS keychain, we need a script, e.g. key-write

wkey-write MY_API_KEY and the script will prompt for entering the key.

wkey-read MY_API_KEY a simple way to check the key has been set properly.

wkey-delete MY_API_KEY removes the key.

I chose to give them quirky names and not make them .profile scripts to add a small degree of security through obscurity. These can be zsh/bash functions but the first thing an attackant is going to do is sift through your profile scripts for clues. They would still need your user's password to read the keys, though. Having them as script makes them more portable...

The scripts

I placed mine in /usr/local/bin/ but you can put them anywhere in your path.

wkey

#!/bin/sh
# usage: wkey KEY [KEY...] -- cmd [args...]
keys=""
while [ "$#" -gt 0 ] && [ "$1" != "--" ]; do keys="$keys $1"; shift; done
[ "$1" = "--" ] && shift
for k in $keys; do
  v=$(security find-generic-password -s "env/$k" -a "$USER" -w) || { echo "wkey: missing $k" >&2; exit 1; }
  export "$k=$v"
done
exec "$@"

wkey-write

#!/bin/sh
# see Notes
# usage: wkey-write KEY_NAME
# Stores a hidden prompt value in the login Keychain as env/KEY_NAME.
set -eu

[ "$#" -eq 1 ] || { echo "usage: wkey-write KEY_NAME" >&2; exit 2; }
key=$1

case $key in
  [A-Za-z_]*) ;;
  *) echo "wkey-write: bad name: $key" >&2; exit 2 ;;
esac
case $key in
  *[!A-Za-z0-9_]*) echo "wkey-write: bad name: $key" >&2; exit 2 ;;
esac

security add-generic-password -s "env/$key" -a "$USER" -T "" -w
echo "wkey-write: stored env/$key" >&2

wkey-read

#!/bin/sh
# usage: wkey-read KEY_NAME
# Prints env/KEY_NAME from the login Keychain to stdout.
set -eu

if [ "$#" -ne 1 ]; then
  echo "usage: wkey-read KEY_NAME" >&2
  exit 2
fi
key=$1

case $key in
  ''|[0-9]*|*[!A-Za-z0-9_]*)
    echo "wkey-read: bad name: $key" >&2
    exit 2
    ;;
esac

if ! value=$(security find-generic-password -s "env/$key" -a "$USER" -w 2>/dev/null); then
  echo "wkey-read: missing $key" >&2
  exit 1
fi
printf '%s\n' "$value"

wkey-delete

#!/bin/sh
# usage: wkey-delete KEY_NAME
# Deletes env/KEY_NAME from the login Keychain.
set -eu

if [ "$#" -ne 1 ]; then
  echo "usage: wkey-delete KEY_NAME" >&2
  exit 2
fi
key=$1

case $key in
  ''|[0-9]*|*[!A-Za-z0-9_]*)
    echo "wkey-delete: bad name: $key" >&2
    exit 2
    ;;
esac

# security delete-generic-password -s env/OPENAI_API_KEY -a "$USER"
if ! security delete-generic-password -s "env/$key" -a "$USER" >/dev/null 2>&1; then
  echo "wkey-delete: not found or not deleted: env/$key" >&2
  exit 1
fi
echo "wkey-delete: deleted env/$key" >&2

Linux

I don't run linux at the moment but these could be replicated by changing the core tool from keychain's security to using KWallet or GNOME's Keyring.

The basics would be these lines:

# save
secret-tool store --label="env/MY_KEY" service env/MY_KEY account "$USER"

# read
secret-tool lookup service env/MY_KEY account "$USER"

And adapting the scripts to use those.

On Linux, I'm not 100% sure we can enforce prompting for user password every time they are used... that remains to be tested.