All posts

macOS Keychain Tutorial for Developers — Store API Keys the Right Way

We spent three years storing API keys in .env files. Plaintext. No encryption. No auth. One afternoon, we ran find ~/dev -name ".env" and counted 47 of them. Same Stripe key copy-pasted across six projects. Same OpenAI token in repos we hadn't touched in months.

The whole time, there was a hardware-encrypted credential store sitting right there on our Macs. Built into the OS since 10.0. Backed by the Secure Enclave. Protected by Touch ID. We just never used it for developer secrets.

This tutorial covers how to actually use the macOS Keychain for API keys — from the native CLI to a workflow you'll stick with.

What the macOS Keychain actually is

It's not just a password manager. The Keychain is a system-level credential store with two distinct layers.

The login keychain is the legacy layer. A SQLite database at ~/Library/Keychains/login.keychain-db, encrypted with your macOS login password. It unlocks when you log in and stays open until you lock it or your Mac sleeps. Most CLI tools hit this one by default.

The Data Protection Keychain is the modern layer — available on every Mac with Apple Silicon or a T2 chip. It's backed by the Secure Enclave, a physically isolated coprocessor on the SoC with its own encrypted memory and cryptographic engine. Keys generated inside the Enclave never leave the chip. Not into RAM, not into swap, not into a crash dump.

When you store a secret with biometric access controls in the Data Protection Keychain, the Secure Enclave handles all encryption and decryption internally. It only releases the plaintext after a successful Touch ID match. No software exploit can pull the decryption key out — it exists only in silicon.

AES-256
hardware encryption
0
keys extractable from Secure Enclave
200ms
per Touch ID verification

Compare that to a .env file: plaintext on disk, readable by any process running as your user, zero authentication required. The Keychain isn't just better — it's a fundamentally different security model.

Tutorial: using the native security CLI

macOS ships with a built-in command-line tool called security for Keychain operations. Here's how it works for developer secrets.

Storing a secret

# Store an API key in the login keychain
$ security add-generic-password -s "myapp" -a "OPENAI_API_KEY" -w "sk-proj-abc123def456"

# -s = service name (like a category)
# -a = account name (the key name)
# -w = the secret value

Retrieving a secret

# Read the value back
$ security find-generic-password -s "myapp" -a "OPENAI_API_KEY" -w
sk-proj-abc123def456

Updating a secret

# This fails — you can't update in place
$ security add-generic-password -s "myapp" -a "OPENAI_API_KEY" -w "sk-proj-new-value"
security: SecKeychainItemCreateFromContent: The specified item already exists in the keychain.

# You have to delete first, then re-add
$ security delete-generic-password -s "myapp" -a "OPENAI_API_KEY"
$ security add-generic-password -s "myapp" -a "OPENAI_API_KEY" -w "sk-proj-new-value"

Using it in a script

# Load a Keychain secret into an environment variable
export OPENAI_API_KEY=$(security find-generic-password -s "myapp" -a "OPENAI_API_KEY" -w)

# Now use it normally
curl https://api.openai.com/v1/models \
  -H "Authorization: Bearer $OPENAI_API_KEY"

Listing secrets

# There's no clean list command. You get a dump.
$ security dump-keychain | grep -B3 "svce"
    "svce"="myapp"
    "acct"="OPENAI_API_KEY"
    ...

This works. Technically. But after a week of using it across multiple projects, the friction becomes obvious.

Five problems with the security CLI

The native tool has real limitations that make it impractical for daily work.

1. Secret values land in shell history. When you run security add-generic-password -w "sk_live_...", that value gets saved to ~/.zsh_history or ~/.bash_history. Permanently. Any process can read your shell history file.

# Your secret is now here forever
$ history | grep "add-generic-password"
  142  security add-generic-password -s "myapp" -a "STRIPE_KEY" -w "sk_live_4eC39HqLy..."

2. No Touch ID. The security CLI uses the login keychain by default, which authenticates with your macOS password — not Touch ID. Getting biometric auth requires the Data Protection Keychain with Swift code, not the CLI.

3. No update-in-place. You can't change a secret. Delete it, re-add it. Two commands every time you rotate a key.

4. No namespace hierarchy. With 30 secrets across 6 projects, everything's flat. The -s (service) and -a (account) fields give you two levels. That's not enough.

5. Raw values print to stdout. The -w flag prints the plaintext secret straight to your terminal. If you're sharing your screen, streaming, or have an AI agent reading your terminal output — the secret's exposed.

The shell history problem is worse than it sounds
Shell history files are plaintext, readable by any process, and backed up by Time Machine. If you've ever stored a secret using security add-generic-password -w, that value may exist in multiple backup snapshots across multiple drives. Run history -c to clear your current session, but the history file itself needs manual cleanup.

A developer-friendly alternative: NoxKey

We hit every one of these problems when we tried to move our secrets from .env files to the Keychain. The Keychain itself was right — the interface was wrong. So we built NoxKey as a thin wrapper around the same Keychain APIs, designed for how developers actually work.

Here's the same workflow, side by side:

Storing a secret

security CLI
# Value goes into shell history
security add-generic-password \
  -s "myapp" -a "STRIPE_KEY" \
  -w "sk_live_4eC39..."
NoxKey
# Copy value to clipboard first, then:
noxkey set myorg/payments/STRIPE_KEY \
  --clipboard
# Nothing in shell history

Retrieving a secret

security CLI
# Raw value printed to stdout
security find-generic-password \
  -s "myapp" -a "STRIPE_KEY" -w
sk_live_4eC39...
NoxKey
# Encrypted handoff — value never
# visible in terminal
eval "$(noxkey get \
  myorg/payments/STRIPE_KEY)"
# Touch ID → secret in $STRIPE_KEY

Updating a secret

security CLI
# Two commands — delete then re-add
security delete-generic-password \
  -s "myapp" -a "STRIPE_KEY"
security add-generic-password \
  -s "myapp" -a "STRIPE_KEY" \
  -w "sk_live_NEW..."
NoxKey
# Just set it again — overwrites
noxkey set myorg/payments/STRIPE_KEY \
  --clipboard

Listing secrets

security CLI
# Unstructured dump
security dump-keychain | \
  grep -A4 "svce"
NoxKey
# Clean tree, no values exposed
noxkey ls myorg/
# myorg/payments/STRIPE_KEY
# myorg/api/OPENAI_KEY
# myorg/db/PROD_URL  strict

NoxKey uses the Data Protection Keychain with .biometryCurrentSet access controls. Every access requires Touch ID. The org/project/KEY naming gives you a clean hierarchy across all your projects. And the eval pattern means the raw secret value never appears in your terminal or shell history — it loads directly into your environment through an encrypted, self-deleting script.

Practical workflow: migrating from .env to Keychain

Here's the process we used to move 47 projects off .env files. Took an afternoon.

Step 1: Find your .env files

$ find ~/dev -name ".env" -not -path "*/node_modules/*" -not -path "*/.git/*"
/Users/you/dev/webapp/.env
/Users/you/dev/api-server/.env
/Users/you/dev/side-project/.env
...

Step 2: Install NoxKey

Download NoxKey from noxkey.ai.

Step 3: Import each .env file

# The import command reads the file and stores each key in the Keychain
$ noxkey import myorg/webapp .env
✓ Imported 5 secrets

$ noxkey import myorg/api-server ../api-server/.env
✓ Imported 3 secrets

Step 4: Verify the import

# List everything under your org
$ noxkey ls myorg/
myorg/webapp/DATABASE_URL
myorg/webapp/OPENAI_API_KEY
myorg/webapp/STRIPE_SECRET_KEY
myorg/webapp/CLOUDFLARE_API_TOKEN
myorg/webapp/SESSION_SECRET
myorg/api-server/DATABASE_URL
myorg/api-server/API_SECRET
myorg/api-server/WEBHOOK_KEY

# Peek at a value to confirm (shows first 8 chars)
$ noxkey peek myorg/webapp/STRIPE_SECRET_KEY
sk_live_...

Step 5: Update your workflow

Instead of relying on dotenv to auto-load a .env file, load secrets explicitly before running your app:

# Before starting your dev server
$ eval "$(noxkey get myorg/webapp/DATABASE_URL)"
$ eval "$(noxkey get myorg/webapp/OPENAI_API_KEY)"
$ eval "$(noxkey get myorg/webapp/STRIPE_SECRET_KEY)"
$ npm run dev

# Or use session unlock for batch loading
$ noxkey unlock myorg/webapp
# Touch ID once, then all gets skip auth
$ eval "$(noxkey get myorg/webapp/DATABASE_URL)"        # no prompt
$ eval "$(noxkey get myorg/webapp/OPENAI_API_KEY)"       # no prompt
$ eval "$(noxkey get myorg/webapp/STRIPE_SECRET_KEY)"    # no prompt
$ npm run dev

Step 6: Delete the .env files

# The important part
$ rm .env

# Make sure .env is in your .gitignore (it should already be)
$ echo ".env" >> .gitignore

That's it. Your secrets are encrypted in the Keychain, protected by Touch ID, organized by project. No files on disk. No values in shell history.

When to use Keychain vs. other approaches

The Keychain isn't the right tool for everything. Here's how we think about it.

Use the macOS Keychain (via NoxKey) when:

  • You're a solo developer or small team on macOS
  • Your secrets are for local development — API keys, database URLs, tokens
  • You want zero cloud dependencies for credential storage
  • You use AI coding agents and need hardware-backed access control
  • You want offline access without subscription fees

Use 1Password / Bitwarden when:

  • You need to share secrets across a team
  • Your team is cross-platform (Linux, Windows, macOS)
  • You want a single vault for both personal and developer credentials
  • You need cloud sync between multiple machines

Use HashiCorp Vault / Doppler / AWS Secrets Manager when:

  • You're managing secrets at infrastructure scale
  • You need automatic rotation, audit logs, and RBAC
  • Secrets need to flow to CI/CD, containers, and production servers
  • Compliance requirements mandate centralized secret management

Use your CI/CD platform's built-in secrets when:

  • Secrets are only needed during builds and deployments
  • GitHub Actions Secrets, Cloudflare environment variables, and AWS Parameter Store are purpose-built for this

Most developers have a gap between "secrets managed in CI" and "secrets on my laptop." That gap gets filled by .env files. It should be filled by the Keychain.

The macOS Keychain doesn't replace Vault or 1Password. It fills the gap between your CI/CD secrets and your laptop — the gap where .env files live today.

Keeping secrets safe from AI agents

If you use AI coding tools — Claude Code, Cursor, GitHub Copilot — your secrets workflow matters more than it used to. These tools read project files to build context. A .env file is a project file. If it's there, it gets read.

The Keychain approach kills that attack surface. Secrets aren't in any file the agent can access. But there's still a risk: when an agent runs noxkey get in your terminal, it could capture the output.

NoxKey handles this by detecting the agent's process tree. When a request comes from an AI agent, it returns an encrypted handoff instead of the raw value. The secret reaches your environment variables but never enters the agent's conversation context. We wrote more about how this works in our Keychain deep-dive.

Agent runs noxkey get Process tree detected Encrypted handoff returned Secret in env, never in context

Quick reference

Common NoxKey commands for daily use:

# Store a secret (from clipboard — nothing in shell history)
$ noxkey set myorg/project/KEY_NAME --clipboard

# Retrieve a secret (Touch ID required)
$ eval "$(noxkey get myorg/project/KEY_NAME)"

# List all secrets under a prefix
$ noxkey ls myorg/

# Peek at first 8 characters (safe for verification)
$ noxkey peek myorg/project/KEY_NAME

# Import an entire .env file
$ noxkey import myorg/project .env

# Session unlock (one Touch ID, then all gets skip auth)
$ noxkey unlock myorg/project

# Mark a secret as strict (always requires Touch ID, even during sessions)
$ noxkey strict myorg/project/PROD_DB_URL

# End a session early
$ noxkey lock
Key Takeaway
Your Mac has had a hardware-encrypted credential store for over 20 years. The native security CLI makes it painful — no Touch ID, values in shell history, no update-in-place. NoxKey wraps the same Keychain APIs with a developer-friendly interface: noxkey set from clipboard, eval "$(noxkey get)" with encrypted handoff, and org/project/KEY namespacing. Free, offline, no account required.

Download NoxKey

Frequently asked questions

What's the macOS Keychain and how does it differ from file-based storage?
The macOS Keychain is an OS-level credential store. Unlike files on disk, secrets in the Keychain are encrypted using hardware-backed keys managed by the Secure Enclave. Accessing them requires authentication (Touch ID or password). A .env file has none of this — it's plaintext that any process can read without authentication.
Can I use the macOS Keychain from the command line?
Yes. macOS includes the security CLI for Keychain operations. Use security add-generic-password to store values and security find-generic-password -w to retrieve them. The catch: secret values end up in shell history, there's no Touch ID support, and you can't update items in place. NoxKey solves these by wrapping the Keychain APIs with a developer-friendly interface.
Does this work on Intel Macs or only Apple Silicon?
The Data Protection Keychain (with Secure Enclave backing) requires Apple Silicon or a T2 chip (MacBook Pro 2018+, MacBook Air 2018+, Mac Mini 2018, Mac Pro 2019, iMac 2020). Older Intel Macs without a T2 chip use the login keychain — still encrypted, but without hardware isolation. NoxKey falls back to password-based authentication on machines without Touch ID.
How do I migrate my .env files to the macOS Keychain?
Install NoxKey from noxkey.ai and run noxkey import myorg/project .env for each project. This reads every key-value pair from the file and stores it in the Keychain with Touch ID protection. Verify with noxkey ls myorg/project/, then delete the .env file. Takes about a minute per project.
Do AI coding agents like Claude Code or Cursor access Keychain secrets?
AI agents can't read the Keychain directly — unlike .env files, there's no file on disk for them to open. When an agent runs noxkey get, NoxKey detects the agent via process-tree inspection and returns an encrypted handoff. The secret reaches your environment variables but the raw value never enters the agent's context window. More on this in why .env files are a liability with AI agents.