All posts 🔐

macOS Keychain for Developers: A Practical Guide

I tried using the macOS security CLI for a month. I wanted to stop storing API keys in .env files, and I figured the Keychain was right there on my machine — encrypted, hardware-backed, free. How hard could it be?

It took me four minutes to store my first secret. It took me eleven minutes to figure out why I couldn't read it back. And after a month of fighting with security find-generic-password flags, I gave up and built something better.

But here's the thing: my instinct was right. The macOS Keychain is the best credential store most developers never use. The problem was never the Keychain. The problem was the interface.

Two keychains, one name

Most developers don't realize macOS has two fundamentally different keychain systems, and the distinction matters.

Legacy Login Keychain File on disk Password-based unlock upgrade to Modern Data Protection Keychain Secure Enclave backed Touch ID / biometric auth

The login keychain is the legacy system. It's a file on disk (~/Library/Keychains/login.keychain-db), encrypted with your macOS login password. When you "unlock" it, everything inside is accessible until it locks again. This is what the security CLI uses by default. This is also the one that's wrong for developer secrets.

The Data Protection Keychain is the modern system, introduced with the T2 chip and Apple Silicon. It's backed by the Secure Enclave — a physically separate processor on your Mac's SoC. Keys generated in the Secure Enclave never leave the chip. Not into RAM, not into swap, not into a crash dump. The Enclave performs encryption and decryption internally, only releasing the plaintext result after biometric verification.

When you store a secret with Data Protection Keychain access controls requiring biometric auth, here's what actually happens at the hardware level:

  1. The Secure Enclave generates a per-item encryption key
  2. Your secret is encrypted with that key and stored in the keychain database
  3. When you request the secret, the Enclave checks your fingerprint against its stored template
  4. Only after a match does the Enclave decrypt and release the value — through a hardware-isolated channel

This isn't "encryption at rest" in the way most tools use the term. This is hardware-enforced access control where the decryption key literally cannot be extracted, even by Apple, even with physical access to the machine.

The security CLI: a developer's nightmare

The native security command can interact with the Keychain. In theory. In practice, it fights you at every step.

Problems with the security CLI
Secret values land in your shell history. There's no Touch ID support — only password-based auth. Error messages assume familiarity with Apple's Security framework internals. And there's no namespace hierarchy for organizing secrets across projects.

Storing a secret:

# Store an API key (the value is now in your shell history)
$ security add-generic-password -s "myapp" -a "STRIPE_KEY" -w "sk_live_4eC39HqL..."

# Try to update it — surprise, you can't. It errors.
$ security add-generic-password -s "myapp" -a "STRIPE_KEY" -w "sk_live_NEW..."
security: SecKeychainItemCreateFromContent: The specified item already exists in the keychain.

# Delete first, 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..."

Reading a secret:

# This prompts for your Keychain password, not Touch ID
$ security find-generic-password -s "myapp" -a "STRIPE_KEY" -w
sk_live_4eC39HqL...

# And yes, it prints the raw value right to stdout
# Hope nobody is watching your terminal

Listing secrets:

# Dump the entire keychain. Good luck parsing this.
$ security dump-keychain
keychain: "/Users/jasper/Library/Keychains/login.keychain-db"
version: 512
class: "genp"
attributes:
    ...
    "svce"="myapp"
    "acct"="STRIPE_KEY"
    ...

The problems compound fast. Secrets land in shell history. There's no namespace hierarchy — good luck organizing 47 secrets across 8 projects. The security CLI defaults to the login keychain (password-based), not the Data Protection Keychain (biometric). And the error messages assume you have a PhD in Apple's Security framework.

I lasted 31 days before I decided there had to be a better way.

What a developer-friendly Keychain interface looks like

I built NoxKey to wrap the Keychain APIs with an interface that actually makes sense for development workflows. Same Keychain, same Secure Enclave, same hardware guarantees — just without the pain.

Here's the same workflow, side by side:

Storing a secret

security CLI
# Value in shell history
# No Touch ID, no namespacing
security add-generic-password \
  -s "myapp" -a "STRIPE_KEY" \
  -w "sk_live_4eC39..."
NoxKey
# From clipboard — no shell history
# Touch ID enforced, namespaced
noxkey set company/payments/STRIPE_KEY \
  --clipboard

Reading a secret

security CLI
# Raw value printed to stdout
# Password auth only
security find-generic-password \
  -s "myapp" -a "STRIPE_KEY" -w
NoxKey
# Encrypted handoff, Touch ID
# Value never visible in terminal
eval "$(noxkey get \
  company/payments/STRIPE_KEY)"

That eval pattern is deliberate. noxkey get doesn't print the secret. It returns a source command pointing to a self-deleting encrypted script. The raw value never appears in your terminal, never lands in logs, never shows up in shell history.

Listing secrets

security CLI
# Wall of XML-ish output
security dump-keychain | grep -A4 "svce"
NoxKey
# Clean hierarchy, no values shown
noxkey ls company/
# company/payments/STRIPE_KEY
# company/api/OPENAI_KEY
# company/db/PROD_URL  strict

Updating a secret

security CLI
# Delete + re-add dance
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
noxkey set company/payments/STRIPE_KEY \
  --clipboard

How NoxKey wraps the Keychain APIs

How it works under the hood
NoxKey uses Apple's Security framework directly — the same APIs that Safari and iCloud Keychain use. Every item is stored as a kSecClassGenericPassword in the Data Protection Keychain with access control flags set to .biometryCurrentSet. The org/project/KEY naming maps to the Keychain's service and account fields with a consistent prefix, so NoxKey items never collide with anything else in your Keychain.

Under the hood, NoxKey uses Apple's Security framework directly — the same APIs that Safari and iCloud Keychain use. Every item is stored as a kSecClassGenericPassword in the Data Protection Keychain with access control flags set to .biometryCurrentSet. That flag is important: it means if you add or remove a fingerprint, all existing items require re-authentication. A stolen laptop with a new fingerprint enrolled can't access your secrets.

The org/project/KEY naming maps to the Keychain's service and account fields with a consistent prefix, so NoxKey items don't collide with anything else in your Keychain. The CLI binary is code-signed and hardened, which means macOS enforces that only NoxKey can access the items it created.

The honest comparison

📄

.env files

Encryption: None
Auth: None
Shell history safe: N/A
Organization: Per-file
Network: Not required
Cost: Free
Agent guardrails: None

⌨️

security CLI

Encryption: Keychain (password)
Auth: Password, unlock-all
Shell history safe: No
Organization: Flat
Network: Not required
Cost: Free
Agent guardrails: None

🔑

1Password CLI

Encryption: AES-256 (cloud)
Auth: Master password
Shell history safe: Yes
Organization: Vaults
Network: Required (sync)
Cost: $36/yr
Agent guardrails: None

🛡️

NoxKey

Encryption: Secure Enclave
Auth: Touch ID, per-access
Shell history safe: Yes
Organization: org/project/KEY
Network: Not required
Cost: Free
Agent guardrails: Process-tree detection

.env files security CLI 1Password CLI NoxKey
Encryption at rest None Keychain (password) AES-256 (cloud) Secure Enclave
Auth model None Password, unlock-all Master password (biometric unlock available) Touch ID, per-access
Secret in shell history N/A Yes No No
Namespace/hierarchy Per-file Flat Vaults org/project/KEY
Network required No No Yes (sync) No
Cost Free Free $36/yr Free
Works offline Yes Yes Partial Yes
AI agent guardrails None None None Process-tree detection

1Password is excellent for team secrets and consumer passwords. Vault and Doppler are the right call for infrastructure-scale secret management. But for a solo developer or small team on macOS who wants their API keys encrypted, biometrically locked, and locally stored — the Keychain is the answer. It's been there the whole time.

The limitation you should know about

This is macOS only. The Secure Enclave is Apple hardware. The Data Protection Keychain is an Apple API. If your team is cross-platform, this isn't a universal solution — it's a local one. Your teammates on Linux will need a different approach.

I'm fine with that trade-off. My secrets are on my machine, for my development workflow. They don't need to sync to a server. They don't need to work on Windows. They need to be encrypted, authenticated, and fast. The Keychain delivers all three.

Getting started in 60 seconds

If you want to try this today:

# Install NoxKey
brew install no-box-dev/noxkey/noxkey

# Import your existing .env file
noxkey import myorg/project .env
# → Touch ID once, all keys stored

# Verify they're there
noxkey ls myorg/project/

# Use a secret
eval "$(noxkey get myorg/project/DATABASE_URL)"

# Delete the .env file. You don't need it anymore.
rm .env

That last step is the important one. Once your secrets are in the Keychain, the .env file is just a liability sitting on disk. Delete it.

Your Mac already has the best credential store. You just need the right interface.
Key Takeaway
The macOS Keychain — specifically the Data Protection Keychain backed by the Secure Enclave — is a hardware-encrypted, biometrically authenticated credential store that's already on your Mac. The security CLI makes it painful to use, but with the right interface, it replaces .env files, eliminates secrets from shell history, and gives you per-access Touch ID authentication — all for free, all offline, all local.