All posts 🔐

macOS Keychain for Developers: A Practical Guide

We tried using the macOS security CLI for a month. The goal: stop storing API keys in .env files and use the Keychain instead — encrypted, hardware-backed, free. How hard could it be?

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

But the 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. The distinction matters for credential management.

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. A file on disk (~/Library/Keychains/login.keychain-db), encrypted with your macOS login password. Unlock it once and everything inside is accessible until it locks again. This is what the security CLI uses by default. This is also the wrong choice 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, releasing plaintext only after biometric verification.

When you store a secret with Data Protection Keychain access controls requiring biometric auth, here's what 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 — not by Apple, not with physical access to the machine.

Why the security CLI fails developers

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. Secrets in shell history. No namespace hierarchy — good luck organizing 47 secrets across 8 projects. Defaults to the login keychain (password-based), not the Data Protection Keychain (biometric). And error messages written for Apple Security framework experts.

What a developer-friendly macOS Keychain interface looks like

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

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
# Click + in the menu bar app, or have your
# AI agent call the bundled MCP tool:
noxkey_set(
  account: "company/payments/STRIPE_KEY",
  clipboard: true
)
# → native approval sheet → Touch ID → stored

Reading a secret

security CLI
# Raw value printed to stdout
# Password auth only
security find-generic-password \
  -s "myapp" -a "STRIPE_KEY" -w
NoxKey (agent via MCP)
# Agent calls the MCP tool:
noxkey_get(
  account: "company/payments/STRIPE_KEY"
)
# → returns: source '/tmp/noxkey_a8f3c1.sh'
# Agent runs that in Bash → $STRIPE_KEY loaded
# Raw value never appears in the conversation

That handoff pattern is deliberate. noxkey_get doesn't return the secret — it returns a source command pointing to a self-deleting encrypted script. The agent runs that one line in Bash, the value lands in $STRIPE_KEY as an env var, and the raw value never enters the conversation context.

Listing secrets

security CLI
# Wall of XML-ish output
security dump-keychain | grep -A4 "svce"
NoxKey
# Open the menu bar app for a clean tree view,
# or have an agent call the safe MCP tool:
noxkey_show()
# company/
#   payments/STRIPE_KEY
#   api/OPENAI_KEY
#   db/PROD_URL
# (names only — no Touch ID required)

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 — same path overwrites
noxkey_set(
  account: "company/payments/STRIPE_KEY",
  clipboard: true
)

How NoxKey uses the macOS Keychain APIs

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.

The .biometryCurrentSet flag is important: 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 menu bar app is code-signed and hardened, which means macOS enforces that only NoxKey can access the items it created. AI agents don't shell out to a CLI — they call the bundled MCP server's noxkey_get tool, which returns a self-deleting source command instead of the raw value. For shell processes that aren't going through MCP, NoxKey still walks the process tree and switches to the same encrypted handoff if it finds an AI runtime above it.

Credential management options compared

.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 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.

We're fine with that trade-off. Our secrets are on our machines, for our development workflows. 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. And when those secrets flow to AI coding tools, the Keychain-backed approach means they stay protected.

Getting started with macOS Keychain in 60 seconds

# 1. Install NoxKey from the Mac App Store

# 2. Drag your .env onto the menu bar icon
#    (or have an agent call noxkey_scan + noxkey_admin import)
#    → native review sheet → Touch ID once → all keys stored

# 3. Browse what landed (names only, no Touch ID)
noxkey_show()

# 4. Use a secret from an agent — call the MCP tool,
#    then run the returned source command in Bash
noxkey_get(account: "myorg/project/DATABASE_URL")
# → source '/tmp/noxkey_xxx.sh'  →  $DATABASE_URL is set

# 5. 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 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. Free, offline, local. No cloud sync required.