Crystalline blue padlock with circuit board internals and dissolving translucent cubes on a grid background

Secret Management in the Age of AI Coding Agents

software development

March 2026.
Srđan Marković's profile image

Srđan Marković

CTO

A practical architecture for securing developer credentials against AI agent context ingestion, using a zero-trust shell configuration pattern with macOS Keychain.

AI coding agents read your shell configuration files. Every agent with terminal access - Cursor, Claude Code, Gemini CLI, Codex, Windsurf - can see your .zshenv, .zshrc, and .bashrc. These files are sometimes synced across machines, and visible in process memory to any tool that reads environment variables. If they contain hardcoded API keys (and those keys need to live somewhere), those keys are exposed across all of these surfaces.

This is what most developer environments look like today:

# ~/.zshrc
export BRAVE_API_KEY="BSAx_g0_XUZ...ue5"
export CONTEXT7_API_KEY="ctx...7da"
export ELEVENLABS_API_KEY="sk_33c...f38"
export HF_TOKEN="hf_rAf...lzU"
export KAGGLE_API_TOKEN="KGAT_09c...7b4"
export MATHPIX_APP_KEY="4eb...366"

Here is what yours will look like if you accept the idea behind this article:

# ~/.zshrc
for _key in "${_SECRETS[@]}"; do
  export "$_key"="$(_keychain_get "$_key")"
done

No values anywhere in the file. The credentials live in macOS Keychain, resolved at interactive shell startup. An AI agent reading your dotfiles sees function calls and variable names. The raw credential never appears.

The Architecture#

Your shell config should reference keys, not contain them. The values live in macOS Keychain and are resolved dynamically.

Why .zshenv and .zshrc Must Be Separate#

During our internal implementation, we learned a critical lesson about timing. The .zshenv file runs for every shell invocation: non-interactive subshells, script executions, IDE background processes. Keychain lookups in that context fail silently.

The solution is a clean architectural split:

  • .zshenv is purely declarative. It defines the _SECRETS array and helper functions. No values, no I/O.
  • .zshrc is the executor. It runs only for interactive shells, where Keychain access is reliable. This is where loading occurs.
Keychain-based secret management architecture

Figure 1: .zshenv defines names and functions. .zshrc resolves values from macOS Keychain at interactive startup.

Here is the complete .zshenv:

# ~/.zshenv - key names and helper functions (no values)

_SECRETS=(
  BRAVE_API_KEY
  CONTEXT7_API_KEY
  ELEVENLABS_API_KEY
  HF_TOKEN
  MATHPIX_APP_KEY
  KAGGLE_API_TOKEN
  # ... add new entries here
)

_keychain_get() {
  local val
  val="$(security find-generic-password -a "$USER" -s "$1" -w 2>/dev/null)" || {
    echo "[secrets] Missing keychain item: $1, add with: _keychain_set $1" >&2
    return 1
  }
  echo "$val"
}

When an AI agent reads this file, it sees an array of names and a function definition. It never sees the raw credential.

Storing Secrets#

Security models fail when they introduce too much friction. If storing a key requires five manual steps, engineers will revert to hardcoding. The _keychain_set helper makes the secure path the easiest path:

# Also in ~/.zshenv
_keychain_set() {
  if security find-generic-password -a "$USER" -s "$1" > /dev/null 2>&1; then
    echo "WARNING: Keychain item '$1' already exists. This will overwrite it permanently (no undo)."
    read -q "?Overwrite? [y/N] " || { echo ""; return 1; }
    echo ""
    security delete-generic-password -a "$USER" -s "$1" > /dev/null 2>&1
  fi
  local secret="${2:-}"
  if [[ -z "$secret" ]]; then
    read -s "secret?Enter value for $1: "
    echo ""
  fi
  security add-generic-password -a "$USER" -s "$1" -w "$secret"
}

Adding a new secret is exactly two steps:

  1. Run _keychain_set BRAVE_API_KEY (prompts for the value securely).
  2. Add BRAVE_API_KEY to the _SECRETS array in ~/.zshenv.

The read -s flag is critical. It suppresses terminal echo, so the plaintext value never appears on screen, in shell history (.zsh_history), or in ps output.

Loading at Startup#

The .zshrc loading block is three lines:

# ~/.zshrc - resolve secrets at interactive shell startup
for _key in "${_SECRETS[@]}"; do
  export "$_key"="$(_keychain_get "$_key")"
done

That is the entire runtime. On macOS, the login keychain is automatically unlocked when you log into the GUI session. No password prompt, no biometric input, no delay beyond the Keychain I/O.

The Cost#

A hardcoded .zshrc adds effectively zero latency to shell startup. Here is the benchmark for loading 15 keys dynamically:

ApproachStartup Time (15 keys)Authentication
Hardcoded .zshrc~0.0sNone
macOS Keychain~0.9sAuto-unlocked on GUI login

Under one second of added latency to remove credentials from AI agent context windows.

Portability#

The Keychain integration is macOS-specific, but the pattern is pure POSIX shell. On Linux, swap _keychain_get for pass (GPG-encrypted), gnome-keyring, or secret-tool (libsecret). The _SECRETS array, the .zshenv/.zshrc split, and the startup loop work identically across any Unix system.

What Comes Next#

This setup eliminates plaintext secrets from your dotfiles on a single machine. Two real-world scenarios remain:

Multi-machine synchronization. If you manage secrets across multiple workstations, the Keychain backend requires manual provisioning on each one. To solve that, we have successfully used 1Password CLI as a cloud-synced backend that resolves all keys in a single authenticated call with Touch ID (Part 2).

SSH access. The moment you SSH into your workstation, macOS deliberately restricts Keychain access and blocks biometric prompts. The solution that worked for us was combining 1Password Service Accounts with a session-aware loading strategy that bypasses biometrics entirely on remote connections (Part 3).

Share