2026-05-15
Secrets in de Keychain, niet in je .env
Een Bash-wrapper van 230 regels rond de macOS Keychain waarmee ik API-keys per commando inject — zonder ze ooit te exporteren naar mijn shell of ze in een .env te committen.
Het probleem
Wie iets met cloud-API’s, databases of AI-agents doet, verzamelt al snel een hoop langlevende credentials. Ze landen op precies de verkeerde plekken:
.enven~/.zshrc, waar één verkeerdegit addze op GitHub zet.export PGPASSWORD=..., dat vervolgens in je shell-omgeving leeft en door elk childproces leesbaar is tot het einde van je sessie.- Je shell-history, waar
mysql -p'…'en vrienden gewoon in platte tekst staan.
De diepere kwestie: er is geen wrijvingsloze manier om een secret
aan één commando te geven en alleen dat commando zonder dat het
in de omgeving lekt. AI-agents maken het erger — ze roepen continu
de shell aan, en alles wat je export-eert valt vanaf dat moment
binnen hun blast radius.
Wat ik eigenlijk wil
Maar twee dingen:
- De credential staat ergens versleuteld op schijf, ontgrendeld zodra ik op mijn Mac inlog.
mycommand --token=$(secret get foo) ...werkt. De waarde stroomt naar dat ene commando en sterft daar.
Meer niet. Geen daemon. Geen master password. Geen extra trust root.
Waarom de gangbare oplossingen tekortschieten
Een paar veelgebruikte opties en waar ze stoppen:
- dotenv-bestanden. Het encryptieverhaal is “niet committen.”
Eenmaal geladen worden ze nog steeds in de shell
export-ed. pass/gopass. Sterke tools, maar je sleept GPG-sleutels, een agent en een aparte trust root binnen. Overkill als je laptop al een OS-keychain heeft.- 1Password CLI. Top als je er al voor betaalt. Voegt een netwerkafhankelijkheid en een sessietoken toe dat je in leven moet houden.
- Direct
securityaanroepen. macOS heeft al een Keychain. De binary werkt (security find-generic-password -a foo -s bar -w), maar is vreselijk om te tikken, geeft niets terug bij een ontbrekende key, en mengt je secrets stilletjes met elk Wi-Fi-wachtwoord en Safari-login die het OS heeft opgeslagen.
De oplossing
230 regels Bash rond security. Vier ontwerpkeuzes die ruwe
Keychain-toegang veranderen in iets dat ik daadwerkelijk gebruik.
- OS-niveau encryptie. Waarden staan in
~/Library/Keychains/login.keychain-db. Versleuteld op schijf, ontgrendeld door je macOS-loginwachtwoord. Geen nieuwe daemon, geen master password. - Ter plekke injecteren. Het aanbevolen patroon is
$(secret get name)— de waarde stroomt naar één commando en verdwijnt daarmee. Komt niet in mijn shell-omgeving, niet in mijn history, niet in een ander childproces dan het bedoelde. - Afgescheiden van de rest van de Keychain. Elk item gebruikt
hetzelfde vaste
account=agent-secrets-veld.secret listfiltert daarop, dus je ziet alleen wat je er zelf in hebt gezet — niet de OS-rommel. - Faalt luid.
secret getexit 1 met een stderr-melding als het item ontbreekt, leeg is, of de Keychain vergrendeld is. Het geeft géén lege string terug die je script daarna een ongeauthenticeerd verzoek tegen prod laat afvuren.
Hoe het er in de praktijk uitziet:
# Eenmaal opslaan, met beschrijving
secret add aliyun-prod-access-key "Aliyun prod root AK"
# Gebruiken zonder te lekken
ALIBABA_CLOUD_ACCESS_KEY_ID=$(secret get aliyun-prod-access-key) \
aliyun ecs DescribeInstances
# Bekijken wat er staat
secret list -l
Code is open: zhaidewei/secret-cli.
Waarom dit prettig is voor AI-agents
De vorm blijkt bijzonder goed te passen op coding agents en andere shell-out-tools:
- Credentials komen nooit in de context van de agent. Wanneer
de agent
cmd --token=$(secret get foo)aanroept, doet de shell de substitutie vóórexec— de waarde belandt in de argv van het childproces, maar de letterlijke string die de agent ziet, logt en herhaalt is$(secret get foo). De ruwe credential staat nooit in het conversatie-transcript. - Kleinste blast radius voor shell-out. Agents shellen constant
uit. Alles wat je
export-eert leeft de rest van de sessie en wordt door elk sibling tool-call geërfd.$(secret get …)leeft één commando en sterft. Gaat de agent later van het pad af, dan ligt de credential niet meer in de omgeving klaar om gestolen te worden. - Luid falen is precies wat een agent nodig heeft. Agents missen het menselijke “hè, waarom is dit leeg?”-instinct. Exit 1 plus een stderr-melding laat de agent het meteen merken en hulp vragen, in plaats van stilletjes een ongeauthenticeerd verzoek tegen prod af te vuren.
- Zelf-beschrijvende inventaris.
secret list -llaat de agent zelf zien wat er is en waar elke entry voor dient — het beschrijvingsveld is meteen ook machine-leesbare metadata, dus ik hoef geen apartecredentials.mdbij te houden voor de agent. - Niet-interactief van nature.
secret getwerkt zonder tty;secret addleest van stdin als het gepiped is. Niets stagneert op “druk op een toets.”
Wat ik hieruit meeneem
De meeste “zware” secret managers gaan ervan uit dat je geen OS hebt om op te leunen. Op een Mac heb je die wel, en je hebt er al voor betaald met je loginwachtwoord. Het interessante werk zat in het dunne ergonomische laagje erboven — namespacing, luid falen, eenmalige injectie — niet in de opslag. De goedkope zet was om niet meer met het OS te vechten, maar het in plaats daarvan te wrappen.
Gedachten hierover? Discussieer met mijn agent, of stuur me een bericht.