Storing API Keys in macOS Keychain

Since macOS 15, the Passwords app covers website logins and passkeys with no GUI entry for generic items, so API keys are stored and read with the security command as generic passwords.

On macOS we read and write Keychain with the security command to store API keys. This article expands the Keychain section of “API Keys Don’t Belong in Your Shell Config Files”, focusing on command-line CRUD.

GUI changes

Since macOS 15 Sequoia, website logins, Wi-Fi passwords, and passkeys moved into Passwords, while Keychain Access moved to the Other folder. Spotlight searches for Keychain Access redirect to Passwords.

Passwords covers only those three categories and shows no generic application passwords. Under Sequoia, creating a Secure Note fails: New brings up only a New Password dialog, and the File > New Password Item flow does not respond.

Generic items like API keys have no corresponding GUI entry. The command line is the working entry point.

Item type

API keys live in generic password items, located by service, account, and password:

  • service is the project or app name, e.g. MyApp
  • account is the key name, e.g. OPENAI_API_KEY
  • password is the key value

internet password carries extra fields such as server, protocol, and port. It serves website logins, not API keys.

Command-line CRUD

We take the login keychain as the default target. Commands without an explicit keychain file read and write it.

Create

Omitting the value after -w switches to interactive input, keeping the secret out of shell history:

security add-generic-password -s "MyApp" -a "OPENAI_API_KEY" -w

Use label and comment for notes:

security add-generic-password -s "MyApp" -a "OPENAI_API_KEY" -l "MyApp OpenAI" -j "prod key 2026-09" -w

Read

-w prints only the password value, -g prints the full item with the password:

security find-generic-password -s "MyApp" -a "OPENAI_API_KEY" -w
security find-generic-password -s "MyApp" -a "OPENAI_API_KEY" -g

Update

-U overwrites when the item already exists. Without -U, the command errors on an existing item:

security add-generic-password -U -s "MyApp" -a "OPENAI_API_KEY" -w

Without -U, delete-then-add reaches the same result:

security delete-generic-password -s "MyApp" -a "OPENAI_API_KEY"
security add-generic-password -s "MyApp" -a "OPENAI_API_KEY" -w

Delete and list

Delete one item and list all items:

security delete-generic-password -s "MyApp" -a "OPENAI_API_KEY"
security dump-keychain | grep -A4 "svce"

dump-keychain output covers all items with metadata.

Calling from scripts and code

Shell reads once at startup and injects into an environment variable. The secret is never written to a file:

export OPENAI_API_KEY=$(security find-generic-password -s "MyApp" -a "OPENAI_API_KEY" -w)

Python calls Keychain through keyring, with service and account matching the command-line triplet:

import keyring
keyring.set_password("MyApp", "OPENAI_API_KEY", "sk-...")
api_key = keyring.get_password("MyApp", "OPENAI_API_KEY")

Native development calls the Security framework, querying with kSecClassGenericPassword plus kSecAttrService and kSecAttrAccount. Node calls through keytar, Go through go-keychain, with identical field semantics.

Practical notes

Practical notes fall into three places: history leakage, authorization popup, and sync scope.

First, history leakage. security add-generic-password -w sk-... writes the secret into shell history and the process table. We avoid plaintext in history and the process table with interactive input or security -i interactive mode.

Second, authorization popup. Items land in the login keychain, which stays unlocked after login. The creating process reads directly, while other apps trigger a system authorization dialog on first read. Clicking Allow Always silences it afterward. CI or SSH hosts with no GUI session cannot click the dialog away. Pre-scope the allowed app with -T /Applications/xxx.app, or switch to a dedicated keychain file. -A allows any app to read, leaving access unrestricted.

Third, sync scope. iCloud-synced items follow the account across machines. High-privilege API keys stay in the local login keychain and migrate with the keychain file.

References