How to Stop Leaking Secrets to AI
Using 1Password and Vault CLI to inject secrets at runtime — keeping them off AI servers
Are You Just Handing Secrets to AI?
When using AI coding tools like Claude Code or Cursor, there are moments when you need to pass API keys or passwords. Deploying scripts, integrating external APIs, setting up infrastructure.
And habitually, you do this:
# Don't do this
export API_KEY="sk-1234567890abcdef"Or you toss them in a .env file and let the AI read it.
The problem is that these secrets can be transmitted to AI servers. If it's in the prompt, it goes over the wire. If you ask the AI to read a file and it slurps up your .env? That content lands in the context window.
For personal projects, maybe you shrug it off. But for organizational secrets, it's a different story.
.env Files Have Become a Liability
The problem with .env files isn't just AI exposure.
Malware targeting .env files has surged recently. Malicious npm packages, VS Code extensions, even GitHub Actions — they read .env at build or install time and exfiltrate the contents.
.env is ultimately a plaintext file. It sits on disk, and anything with read permissions can see it. Every process with filesystem access is a potential threat.
The key insight: Secrets should live in a secure store, accessible only when you explicitly approve it. Fingerprint, password, whatever — a human must authorize the moment of retrieval.
Outside the Org: 1Password CLI
For personal projects and external work, I use 1Password CLI (op).
Most people use 1Password as a password manager but don't know it has a CLI.
Installation
# macOS
brew install --cask 1password-cli
# Linux
# https://developer.1password.com/docs/cli/get-started/Store Your Secrets
Store secrets in the 1Password app. Say you saved an "OpenAI API Key" entry.
Runtime Injection
# Inject as env var — pulled from 1Password at execution time
export OPENAI_API_KEY=$(op read "op://Personal/OpenAI API Key/credential")When this runs, 1Password prompts for authentication. Touch ID or master password required before the value is returned.
There's a cleaner approach. op run lets you define references in a template file and substitutes real values at runtime.
# .env.tpl (no actual secrets in this file)
OPENAI_API_KEY=op://Personal/OpenAI API Key/credential
DATABASE_URL=op://Development/PostgreSQL/connection-string
# Run
op run --env-file=.env.tpl -- npm run dev.env.tpl is safe to commit to Git. It only contains 1Password paths, not actual values.
If AI reads this file, it only sees op://Personal/OpenAI API Key/credential — not the actual key.
Using with AI Tools
# Run Claude Code within 1Password context
op run --env-file=.env.tpl -- claude
# Or inject specific secrets
ANTHROPIC_API_KEY=$(op read "op://Dev/Anthropic/api-key") claudeSecrets exist only as process environment variables, never as plaintext on the filesystem. The AI could run env to see them, but that's far safer than having a .env file that can be read and transmitted wholesale.
Inside the Org: HashiCorp Vault
Inside the organization, we use HashiCorp Vault instead of 1Password.
Vault is purpose-built for secret management. Access control, audit logs, automatic rotation. And the key point — Vault has a CLI too.
Vault CLI Basics
# Store secrets
vault kv put secret/myapp/api-keys openai="sk-xxx" anthropic="sk-yyy"
# Retrieve secrets
vault kv get -field=openai secret/myapp/api-keysRuntime Injection Pattern
Same pattern as 1Password's op read.
# Inject as env var
export OPENAI_API_KEY=$(vault kv get -field=openai secret/myapp/api-keys)
# Or in a script
#!/bin/bash
OPENAI_API_KEY=$(vault kv get -field=openai secret/myapp/api-keys) \
ANTHROPIC_API_KEY=$(vault kv get -field=anthropic secret/myapp/api-keys) \
claudeAccess Control: AppRole Tokens
Vault's strength is granular access control.
# Define a read-only policy for a specific path
vault policy write ai-readonly - <<EOF
path "secret/data/myapp/api-keys" {
capabilities = ["read"]
}
EOF
# Create an AppRole
vault auth enable approle
vault write auth/approle/role/ai-agent \
token_policies="ai-readonly" \
token_ttl=1h \
token_max_ttl=4hAI agent tokens are read-only + short TTL. If a token leaks, it expires in an hour. If a .env file leaks? The keys are valid forever until you manually rotate them.
Storing AppRole Tokens in Keychain
One more layer. The AppRole token itself can be stored in the system Keychain.
# Store Vault token in macOS Keychain
security add-generic-password \
-a "vault-ai-agent" \
-s "vault-token" \
-w "hvs.CAESIG..." \
-T "" \
/Library/Keychains/System.keychain
# Retrieve from Keychain for Vault auth
export VAULT_TOKEN=$(security find-generic-password \
-a "vault-ai-agent" \
-s "vault-token" \
-w)Keychain has a CLI too. macOS has security, Linux has secret-tool (libsecret).
This way, even the auth token never exists as plaintext on the filesystem. Accessing the Keychain requires system authentication (fingerprint, password). Set it up once, and the Keychain keeps it safe.
The Full Flow
[Secret Store] [Auth Gate] [Runtime]
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 1Password │────▶│ Touch ID / │────▶│ Injected as │
│ or Vault │ │ Password │ │ env var into │
│ │ │ │ │ process │
└──────────────┘ └──────────────┘ └──────────────┘
│
AI receives the
value via env var,
not by reading
a file
| .env file | 1Password / Vault | |
|---|---|---|
| Storage | Plaintext | Encrypted store |
| Access control | File permissions only | Biometrics / tokens |
| On leak | Manual key rotation | Token TTL expiry / instant revoke |
| AI server exposure | File read = transmitted | Only path references transmitted |
| Malware resistance | Defenseless | No access without auth |
Practical Setup with Claude Code
Here's how to apply this concretely with Claude Code.
Define Rules in CLAUDE.md
## Secret Management Rules
- Never read .env files directly
- Use `op read` or `vault kv get` for secrets
- Never paste API keys into prompts
- Never cat or read files containing secretsExecution Examples
# With 1Password (personal)
op run --env-file=.env.tpl -- claude
# With Vault (organization)
export VAULT_TOKEN=$(security find-generic-password -a "vault" -s "token" -w)
OPENAI_API_KEY=$(vault kv get -field=openai secret/myapp/keys) claudeThis way, the Claude Code process receives secrets via environment variables, but no plaintext .env exists on the filesystem. Even if AI explores your files, it won't find any secrets.
Conclusion
Secret management boils down to this:
- No secrets in plaintext files —
.envis now malware's #1 target - Use a dedicated secret store — 1Password for personal, Vault for orgs
- Inject only at runtime — pull via CLI at execution time
- Require explicit approval — fingerprint, password, token TTL
As AI tools become more capable, secret management becomes more critical. You can ask AI to write your code, but you don't need to hand over your secrets along with it.
Secrets in the vault, keys in your hands.