LLM Key Ring (lkr)
A secure CLI tool for managing LLM API keys via macOS Keychain. No more plaintext keys in .env files.
$ lkr set openai:prod
Enter API key for openai:prod: ****
Stored openai:prod (kind: runtime)
$ lkr get openai:prod
Copied to clipboard (auto-clears in 30s)
sk-p...3xYz (runtime)
$ lkr exec -- python script.py # Keys injected as env vars (safest)
$ lkr gen .env.example -o .env # Generate config from template
Why?
| Problem | lkr Solution |
|---|---|
API keys sitting in .env files |
Encrypted in macOS Keychain |
| Keys leaked via shell history | Interactive prompt input (never CLI args) |
| AI agents extracting keys via pipe | TTY guard blocks --plain in non-interactive environments |
| Keys lingering in clipboard | Auto-clears after 30 seconds |
| Keys lingering in process memory | zeroize wipes memory on drop |
Install
# From source
Requires Rust 1.85+ and macOS (uses native Keychain).
Usage
Store a key
|
Key names use provider:label format (e.g., openai:prod, anthropic:main).
Retrieve a key
List keys
Run a command with keys as env vars (recommended)
Keys are mapped to conventional env var names (e.g., openai:prod → OPENAI_API_KEY) and injected into the child process. Only runtime keys are injected — admin keys are excluded by design. Keys never appear in stdout, files, or clipboard — this is the safest way to pass secrets to programs. Prefer exec over gen whenever possible.
Generate config from template
Use gen when the target program requires a config file and cannot accept env vars:
.env.example format — keys are auto-resolved by exact env var name match:
OPENAI_API_KEY=your-key-here # ← resolved from openai:* in Keychain
ANTHROPIC_API_KEY= # ← resolved from anthropic:*
JSON template format — use explicit {{lkr:provider:label}} placeholders:
Generated files are written with 0600 permissions. A warning is shown if the output file is not in .gitignore.
When multiple runtime keys exist for the same provider (e.g., openai:prod and openai:stg), the alphabetically first key is used. A warning lists alternatives. Use {{lkr:provider:label}} placeholders for explicit control.
Delete a key
Check API usage costs
Requires an Admin API key registered with --kind admin:
Global flags
Supported Providers
lkr gen auto-resolves keys for these providers:
| Provider | Env Variable | Key Name Example |
|---|---|---|
| OpenAI | OPENAI_API_KEY |
openai:prod |
| Anthropic | ANTHROPIC_API_KEY |
anthropic:main |
GOOGLE_API_KEY |
google:dev |
|
| Mistral | MISTRAL_API_KEY |
mistral:api |
| Cohere | COHERE_API_KEY |
cohere:prod |
| Groq | GROQ_API_KEY |
groq:prod |
| DeepSeek | DEEPSEEK_API_KEY |
deepseek:api |
| xAI | XAI_API_KEY |
xai:prod |
| And more... |
Any provider:label name works with set/get/rm. The provider list above is used for auto-resolution in lkr gen.
Security
Threat Model
See docs/SECURITY.md for the full threat model. Key protections:
| Threat | Mitigation |
|---|---|
| Plaintext key files | Keys stored in macOS Keychain (encrypted at rest) |
| Shell history exposure | lkr set reads from prompt, never from CLI arguments |
| Clipboard residual | 30s auto-clear via SHA-256 hash comparison |
| Terminal shoulder-surfing | Masked by default (sk-p...3xYz) |
| AI agent exfiltration | TTY guard (isatty) blocks --plain/--show in non-interactive environments |
| Memory forensics | zeroize::Zeroizing<String> zeroes memory on drop |
| Admin key in templates | lkr gen only resolves runtime keys |
| Accidental git commit | .gitignore coverage check on generated files |
Agent IDE Attack Protection
AI coding assistants (Cursor, Copilot, etc.) can be tricked via prompt injection into running commands that exfiltrate secrets. lkr defends against this with three layers:
What TTY guard protects against: non-interactive exfiltration — agents piping --plain/--show output or reading clipboard via pbpaste.
What it does NOT protect against: a user being socially engineered into running --show in their own terminal. Use lkr exec as the default workflow to minimize this surface.
# Layer 1: --plain/--show blocked in pipes (exit code 2)
|
# Error: --plain and --show are blocked in non-interactive environments.
# Layer 2: Clipboard copy skipped in non-interactive environments
|
# "Clipboard copy skipped (non-interactive environment)."
# → prevents `lkr get key && pbpaste` bypass
# Layer 3: Safe alternatives for automation
Architecture
llm-key-ring/
├── crates/
│ ├── lkr-core/ # Library: KeyStore trait, Keychain, templates, usage API
│ └── lkr-cli/ # Binary: clap CLI (set/get/list/rm/gen/usage/exec)
├── docs/
│ └── SECURITY.md # Threat model
├── LICENSE-MIT
└── LICENSE-APACHE
All business logic lives in lkr-core. The CLI is a thin wrapper.
Platform Support
Currently macOS only (uses native Keychain via security-framework). The KeyStore trait abstraction is designed for future backend support (Linux libsecret, Windows Credential Manager).
Keychain Storage
| Field | Value |
|---|---|
| Service | com.llm-key-ring |
| Account | {provider}:{label} |
| Password | {"value":"sk-...","kind":"runtime"} |
Development
# Build
# Test
# Clippy
# Run without installing
License
Dual-licensed under MIT or Apache-2.0, at your option.