name: pqc-secrets description: Post-quantum cryptography secrets management system for protecting API keys, tokens, and private data.
PQC Secrets Management Agent Skill
Companion skill: pqc-signatures-security — ML-DSA-65 code signing, integrity verification, and secure coding patterns. Use both together for complete PQC coverage: this skill encrypts secrets at rest; pqc-signatures-security verifies code hasn't been tampered with.
This skill provides comprehensive instructions, policies, and blueprints for managing repository and application secrets (API keys, credentials, private user data) using post-quantum cryptographic (PQC) algorithms.
AI agents using this skill are equipped to:
- Protect credentials at rest and in memory using FIPS-compliant algorithms.
- Integrate with the platform's native keychain and standard runtime environments.
- Validate environments and ensure zero plaintext credentials are written to disk.
1. Core Philosophy: Zero Plaintext on Disk
Traditional secrets management relies heavily on plaintext .env files or insecure environment variables. Under the PQC mandate:
- Always protect API keys and private data with FIPS 203 (ML-KEM-768) key encapsulation and AES-256-GCM symmetric encryption.
- Never store raw API keys, secrets, or tokens in git repositories, task files, logs, or local unencrypted files.
- Always load secrets on-demand directly into the shell environment from an encrypted bundle, and ensure they never persist to disk.
1.1 ⚠️ Common Agent Trap: Settings Files with env Blocks
Many CLI tools (Claude Code, VS Code, Docker, etc.) use JSON/YAML settings files with an env block. Agents will naturally want to put API keys directly in these files. This is a PQC violation.
NEVER do this:
{
"env": {
"ANTHROPIC_AUTH_TOKEN": "sk-ss-v1-abc123...",
"ANTHROPIC_API_KEY": "sk-ant-..."
}
}
env:
API_KEY: "sk-..."
{
"env": {
"OPENAI_API_KEY": "sk-proj-..."
}
}
Why this is wrong: The settings file is a regular file on disk. Anyone with file read access (backup tools, cloud sync, other processes, disk imaging) can extract the key. It will also be committed to git if the file is tracked, and it shows up in file search/grep results.
ALWAYS do this instead — inject at runtime from the OS keychain:
# Shell wrapper reads from Keychain and passes via env var
my-tool() {
local api_key
api_key="$(security find-generic-password -s "my-tool" -a "api-key" -w 2>/dev/null)"
if [[ -z "$api_key" ]]; then
echo "Error: API key not found in Keychain." >&2
return 1
fi
ANTHROPIC_AUTH_TOKEN="$api_key" command my-tool "$@"
}
The settings file keeps everything except the secret value:
{
"env": {
"ANTHROPIC_BASE_URL": "https://zenmux.ai/api/anthropic",
"ANTHROPIC_API_KEY": "",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "deepseek/deepseek-v4-flash"
}
}
The key exists only in two places: the OS keychain (encrypted at rest) and process memory (volatile). Never on disk in any config file.
2. Infrastructure Architecture
The PQC secrets system consists of a dual-implementation architecture to ensure maximum performance and cross-platform compatibility:
- Rust Native (Primary): The compiled binary at
bin/pqc-secrets(source insrc/pqc-secrets/) uses the NIST FIPS 203 compliantfips203crate. It implements a secure double-envelope structure:- ML-KEM-768 encapsulates a Key Encapsulating Key (KEK).
- The KEK wraps a Data Encryption Key (DEK) via AES-256-GCM keywrap.
- The DEK encrypts the secret payload via AES-256-GCM.
- Stores metadata including Additional Authenticated Data (
aad) insecrets.bundle.json.
- Python Fallback (Secondary): A script at
.agents/skills/pqc-secrets/scripts/pqc_secrets.pyuseskyber-pyand implements a single-envelope structure (ML-KEM-768 encapsulates the DEK directly).
[!WARNING] Format Incompatibility & Mismatch Errors: Because the Rust binary and Python script use different envelope structures, their serialized bundles are incompatible. Running the Rust binary on a Python-packed bundle results in:
Error: missing field aad at line X column YIf this occurs, you must perform a one-time migration:
# 1. Export plaintext secrets using the Python fallback SECRETS_TXT=$(uv run .agents/skills/pqc-secrets/scripts/pqc_secrets.py export) # 2. Generate a new keypair using the Rust binary ./bin/pqc-secrets keygen # 3. Pack the secrets back into the Rust-compatible bundle echo "$SECRETS_TXT" | ./bin/pqc-secrets pack
The local secrets infrastructure lives at ~/.config/pqc-secrets/:
System Keychain / File ~/.config/pqc-secrets/ ┌──────────────────────┐ ┌────────────────────────────┐ │ macOS Keychain, │ │ recipient.pub │ │ Linux Secret Service,│ │ ML-KEM-768 public key │ │ or Encrypted File │ │ (safe to commit) │ └──────────┬───────────┘ └────────────┬───────────────┘ │ │ │ decaps (ML-KEM-768) │ encaps ▼ ▼ ┌──────────────────────────────────────────────────────────────┐ │ secrets.bundle.json │ │ ┌─────────────────┐ ┌──────────────────────────────────┐ │ │ │ kem.ciphertext │ │ data.ciphertext (AES-256-GCM) │ │ │ │ (ML-KEM-768) │ │ 24+ API keys encrypted at rest │ │ │ └─────────────────┘ └──────────────┬───────────────────┘ │ └──────────────────────────────────────┼───────────────────────┘ │ decrypt ▼ ┌──────────────────────────────────────────────────────────────┐ │ Exported environment variables (never touch disk) │ │ ANTHROPIC_AUTH_TOKEN ZENMUX_API_KEY NEBIUS_API_KEY │ │ OPENROUTER_API_KEY WAFER_API_KEY ... (in-memory only) │ └──────────────────────────────────────────────────────────────┘
3. Cryptographic Standards
For all secrets operations, only NIST-approved post-quantum algorithms are permitted. Traditional classical algorithms are strictly forbidden for protecting key material:
| Use Case | Permitted Algorithms (FIPS) | Forbidden Algorithms |
|---|---|---|
| Key Encapsulation (KEM) | ML-KEM-768, ML-KEM-1024 | RSA, ECDH, ECDSA, Ed25519 |
| Symmetric Encryption | AES-256-GCM | AES-CBC, DES, 3DES, Blowfish, RC4 |
| Digital Signatures | ML-DSA-65/87, SLH-DSA-SHA2-128s | MD5, SHA-1, RSA, DSA |
4. Lifecycle Commands
Use the primary native Rust binary bin/pqc-secrets (or run the Python fallback via uv run .agents/skills/pqc-secrets/scripts/pqc_secrets.py):
| Step | Rust Command | Python Fallback Command | Description |
|---|---|---|---|
| Keygen | bin/pqc-secrets keygen |
uv run .agents/skills/pqc-secrets/scripts/pqc_secrets.py keygen |
Generates a new ML-KEM-768 keypair. Stores the private key in local encrypted file (system keychain if opted-in via PQC_USE_KEYCHAIN=true). Public key → ~/.config/pqc-secrets/recipient.pub. |
| Pack | bin/pqc-secrets pack |
uv run .agents/skills/pqc-secrets/scripts/pqc_secrets.py pack |
Reads KEY=VAL lines from stdin, encrypts via AES-256-GCM + ML-KEM-768, writes secrets.bundle.json. |
| Load | bin/pqc-secrets export |
uv run .agents/skills/pqc-secrets/scripts/pqc_secrets.py export |
Decrypts bundle in-memory, outputs export KEY=VALUE lines. Use secrets-load shell function. |
| Verify | bin/pqc-secrets verify |
uv run .agents/skills/pqc-secrets/scripts/pqc_secrets.py verify |
Verifies bundle can be decrypted, lists key names. |
| Rotate | bin/pqc-secrets keygen && bin/pqc-secrets pack |
keygen && pack |
Generates new keypair and re-packs secrets under new public key. |
| Rewrap | bin/pqc-secrets rewrap --new-pub <path> --out <path> |
— | Re-encrypts bundle under a different public key without exposing plaintext. |
| Migrate | bin/pqc-secrets migrate |
uv run ... pqc_secrets.py migrate |
Migrates keychain entry from old account name to new. |
Migration from Legacy default Account
If you have an existing keychain entry with account name default (from older versions), migrate to pqc-secrets-key:
bin/pqc-secrets migrate
# Or with custom account names:
PQC_KEYCHAIN_ACCOUNT_OLD=default PQC_KEYCHAIN_ACCOUNT_NEW=pqc-secrets-key bin/pqc-secrets migrate
Environment Variables
| Variable | Default | Description |
|---|---|---|
PQC_KEYCHAIN_ACCOUNT |
pqc-secrets-key |
|
PQC_CONFIG_DIR |
~/.config/pqc-secrets |
|
PQC_USE_KEYCHAIN |
false |
Implementation Details
- Rust Primary Engine:
fips203crate (rust-fips203, NSA CNSA 2.0 / FIPS 203 compliant ML-KEM-768). - Rust Primary Dependencies:
security-framework(macOS Keychain),aes-gcm,serde,serde_json,sha3. - Python Fallback Engine:
kyber-py(pure Python ML-KEM-768) +cryptography(AES-256-GCM). - Python Fallback Dependencies: Managed inline via UV script metadata —
kyber-py>=0.2.0,cryptography>=44.0(auto-resolved byuv run).
5. Application Integration Guidelines
Applications must read secrets exclusively from environment variables populated dynamically in memory. Do not store or read plaintext files inside the application context.
Pattern 1: Safe Environment Variable Consumption (Python)
import os
import sys
def get_api_key(name: str) -> str:
"""Retrieve secret from environment, ensuring no fallback to disk files."""
api_key = os.environ.get(name)
if not api_key:
print(f"CRITICAL ERROR: Environment variable '{name}' is not set.", file=sys.stderr)
print("Please load secrets via 'secrets-load' before running this command.", file=sys.stderr)
sys.exit(1)
return api_key
Pattern 2: Safe Environment Variable Consumption (Node.js)
function getApiKey(name) {
const apiKey = process.env[name];
if (!apiKey) {
console.error(`CRITICAL ERROR: Environment variable '${name}' is not set.`);
console.error("Please load secrets via 'secrets-load' before running this application.");
process.exit(1);
}
return apiKey;
}
Pattern 3: Safe Environment Variable Consumption (Rust)
fn get_api_key(name: &str) -> String {
std::env::var(name).unwrap_or_else(|_| {
eprintln!("CRITICAL ERROR: Environment variable '{}' is not set.", name);
eprintln!("Please load secrets via 'secrets-load' before running this command.");
std::process::exit(1);
})
}
Pattern 4: Shell Wrapper Integration
When wrapping command-line tools or other agents, pass env variables down rather than writing to disk config files. If a configuration file is strictly required by the tool (e.g. auth.json or .env file), write it dynamically to a secure directory (or temporary RAM disk if supported) and delete it immediately upon tool exit via traps.
# Example wrapper with exit trap for temporary configs
run_tool_with_secrets() {
local temp_env
temp_env=$(mktemp /tmp/tool-env.XXXXXX)
trap 'rm -f "$temp_env"' EXIT
# Populate temp config from environment variables loaded via secrets-load
cat > "$temp_env" <<EOF
API_KEY="${ZENMUX_API_KEY}"
EOF
command-tool --config "$temp_env" "$@"
}
Pattern 5: Headless & CI/CD Pipelines
In headless environments where macOS Keychain is unavailable, use standard platform secrets injection (e.g., GitHub Secrets, Kubernetes Secrets, or Docker environment flags) passed dynamically from standard input.
# Injecting secrets directly via stdin to avoid write-to-disk
docker run -e ZENMUX_API_KEY=$(bin/pqc-secrets export | grep ZENMUX_API_KEY | cut -d= -f2) my-image