Keep Secrets Out of Files: Using pass to Secure Your API Keys
The problem with .env files is simple: they’re files on disk. Unencrypted, trackable, leakable. A better approach is to store secrets encrypted and fetch them at runtime—only when you actually need them.
Enter pass: a minimal password manager for Unix/Linux/macOS that encrypts secrets with GPG. Your secrets stay encrypted on disk. Only during runtime do they exist in memory.
Part 1: Why pass Beats .env Files
With .env files:
- Secrets sit unencrypted on disk ❌
- Accidental commits leak everything ❌
- Every file that reads them must handle plaintext ❌
- One misconfigured backup exposes everything ❌
With pass:
- Secrets encrypted with GPG on disk ✅
- Requires password to decrypt (even locally) ✅
- Secrets only in memory during runtime ✅
- No file access = no leaks ✅
- Works great with Git (encrypted store is safe to commit) ✅
Part 2: Setup (One-Time, 10 Minutes)
2.1 Install pass
macOS:
brew install pass
Linux (Debian/Ubuntu):
sudo apt-get install pass
Linux (Fedora/RHEL):
sudo dnf install pass
Verify:
pass --version
# Should output: pass, the standard unix password manager
2.2 Set Up GPG (If You Don’t Have Keys Yet)
pass encrypts with GPG, so you need a GPG key. Check if you have one:
gpg --list-secret-keys
If nothing appears, create a new key:
gpg --gen-key
# Answer the prompts:
# - Real name: Your Name
# - Email: your-email@example.com
# - Passphrase: STRONG passphrase (you'll type this when accessing secrets)
This creates an encryption key that protects your secrets. The passphrase is what encrypts everything.
Get your key ID:
gpg --list-secret-keys --keyid-format SHORT
# Output:
# sec rsa4096/ABC12345 2026-02-24 [SC]
# ↑ Your key ID is ABC12345
2.3 Initialize the pass Store
pass init ABC12345
# Output: Password store initialized for ABC12345
# Verify:
ls -la ~/.password-store/
This creates ~/.password-store/ where all your encrypted secrets live.
2.4 Add Your First Secret
pass insert todoist/api-key
# Prompts for password (enter it twice)
pass insert slack/bot-token
pass insert gemini/api-key
pass insert github/personal-token
View what you’ve stored (encrypted directory):
pass
# Output:
# Password Store
# ├── todoist
# │ └── api-key
# ├── slack
# │ └── bot-token
# ├── gemini
# │ └── api-key
# └── github
# └── personal-token
Retrieve a secret:
pass todoist/api-key
# Prompts for GPG passphrase (your password)
# Returns the plaintext key (only in memory)
Part 3: Fetch at Runtime (Bash)
3.1 Simple Bash Script
Create a startup script that fetches secrets only when needed:
#!/bin/bash
# scripts/setup-secrets.sh
# Fetch from pass and export as environment variables
export TODOIST_API_KEY=$(pass todoist/api-key)
export SLACK_BOT_TOKEN=$(pass slack/bot-token)
export GEMINI_API_KEY=$(pass gemini/api-key)
# Run your OpenClaw agent
openclaw status
Make it executable:
chmod +x scripts/setup-secrets.sh
./scripts/setup-secrets.sh
What happens:
passprompts for your GPG passphrase- Secrets are decrypted and loaded into memory (not written to disk)
- Environment variables are set
- OpenClaw runs with access to the secrets
- When the script exits, environment variables are cleared
3.2 Batch Fetch for Multiple Secrets
#!/bin/bash
# scripts/fetch-all-secrets.sh
# Array of secret paths in pass
SECRETS=(
"todoist/api-key:TODOIST_API_KEY"
"slack/bot-token:SLACK_BOT_TOKEN"
"gemini/api-key:GEMINI_API_KEY"
"github/personal-token:GITHUB_TOKEN"
)
echo "Fetching secrets from pass..."
for secret in "${SECRETS[@]}"; do
path="${secret%:*}"
var="${secret#*:}"
export "$var"=$(pass "$path")
echo "✓ Loaded $var"
done
echo "Secrets ready. Running OpenClaw..."
openclaw status
3.3 Error Handling
#!/bin/bash
# scripts/setup-secrets-safe.sh
set -euo pipefail # Exit on error
# Function to fetch secret safely
fetch_secret() {
local path="$1"
local var="$2"
if ! secret=$(pass "$path" 2>/dev/null); then
echo "Error: Could not fetch $path from pass"
echo "Make sure you've run: pass insert $path"
return 1
fi
export "$var"="$secret"
echo "✓ $var loaded"
}
# Fetch all secrets with error checking
fetch_secret "todoist/api-key" "TODOIST_API_KEY" || exit 1
fetch_secret "slack/bot-token" "SLACK_BOT_TOKEN" || exit 1
fetch_secret "gemini/api-key" "GEMINI_API_KEY" || exit 1
echo "All secrets loaded. Running OpenClaw..."
openclaw status
Part 4: Fetch at Runtime (Python)
4.1 Simple Python Helper
#!/usr/bin/env python3
# scripts/fetch_secrets.py
import subprocess
import os
import sys
def get_secret(path):
"""Fetch a secret from pass at runtime."""
try:
result = subprocess.run(
['pass', path],
capture_output=True,
text=True,
check=True
)
return result.stdout.strip()
except subprocess.CalledProcessError:
raise ValueError(f"Could not fetch secret: {path}")
except FileNotFoundError:
raise FileNotFoundError("pass is not installed. Run: brew install pass")
# Fetch secrets and set environment variables
secrets = {
'TODOIST_API_KEY': 'todoist/api-key',
'SLACK_BOT_TOKEN': 'slack/bot-token',
'GEMINI_API_KEY': 'gemini/api-key',
'GITHUB_TOKEN': 'github/personal-token',
}
print("Fetching secrets from pass...")
for var_name, secret_path in secrets.items():
try:
secret_value = get_secret(secret_path)
os.environ[var_name] = secret_value
print(f"✓ {var_name} loaded")
except Exception as e:
print(f"✗ Failed to load {var_name}: {e}")
sys.exit(1)
print("All secrets loaded. Ready to run OpenClaw.")
# Now you can use the secrets:
print(f"Todoist API Key length: {len(os.environ['TODOIST_API_KEY'])}")
Usage:
python3 scripts/fetch_secrets.py
# Or run OpenClaw with secrets:
python3 scripts/fetch_secrets.py && openclaw status
4.2 Context Manager for Safe Secret Handling
#!/usr/bin/env python3
# scripts/secrets.py
import subprocess
import os
from contextlib import contextmanager
@contextmanager
def load_secrets(secret_paths):
"""Context manager: loads secrets, runs code, then clears them."""
saved_env = os.environ.copy()
try:
# Load all secrets
for var_name, secret_path in secret_paths.items():
result = subprocess.run(
['pass', secret_path],
capture_output=True,
text=True,
check=True
)
os.environ[var_name] = result.stdout.strip()
print("✓ Secrets loaded")
yield
finally:
# Restore original environment (clear secrets)
os.environ.clear()
os.environ.update(saved_env)
print("✓ Secrets cleared from memory")
# Usage:
if __name__ == '__main__':
secrets = {
'TODOIST_API_KEY': 'todoist/api-key',
'GEMINI_API_KEY': 'gemini/api-key',
}
with load_secrets(secrets):
# Your code here has access to TODOIST_API_KEY, GEMINI_API_KEY
api_key = os.environ['TODOIST_API_KEY']
print(f"Using API key: {api_key[:10]}...")
# After the with block, secrets are cleared from memory
print(f"TODOIST_API_KEY now: {os.environ.get('TODOIST_API_KEY', 'NOT SET')}")
Part 5: How Secrets Stay Encrypted
Why This Is Safe
At Rest (On Disk):
- Secrets live in
~/.password-store/encrypted with your GPG key - Even if someone steals your hard drive, they can’t read them without your GPG passphrase
- Safe to back up, safe to version control (if needed)
- Secrets live in
At Runtime (In Memory):
passdecrypts only when you request it- Decrypted secret exists in memory for seconds/minutes
- When your script exits, memory is reclaimed by the OS
- Modern OSes don’t persist memory to unencrypted swap
Never in Log Files:
- Secrets are never written to disk as plaintext
- OpenClaw logs don’t contain your API keys (assuming you’re not logging
os.environ) - Git history stays clean (no accidental commits)
GPG Protection
# Your GPG key protects everything:
ls -la ~/.password-store/todoist/
# -rw-r--r-- api-key.gpg
# This file is encrypted binary. Without your passphrase:
cat ~/.password-store/todoist/api-key.gpg
# Outputs binary gibberish, not your key
Memory Safety
When secrets are in environment variables:
# ✗ BAD - Secret visible in process listing
export TODOIST_API_KEY="abc123xyz"
sleep 3600 # Long-running process
# ✓ GOOD - Secret loaded on demand, cleared on exit
fetch_and_run "todoist/api-key" "openclaw run my-agent"
Part 6: Using with OpenClaw
6.1 Store OpenClaw API Keys in pass
# Add your OpenClaw-related secrets
pass insert openclaw/todoist-api-key
pass insert openclaw/slack-token
pass insert openclaw/gemini-key
6.2 OpenClaw Integration Script
#!/bin/bash
# scripts/run-openclaw-with-secrets.sh
set -euo pipefail
# Load secrets from pass
export TODOIST_API_KEY=$(pass openclaw/todoist-api-key)
export SLACK_BOT_TOKEN=$(pass openclaw/slack-token)
export GEMINI_API_KEY=$(pass openclaw/gemini-key)
echo "✓ Secrets loaded from pass"
# Run OpenClaw with secrets in environment
openclaw status
# Secrets are automatically cleared when script exits
echo "✓ OpenClaw finished. Secrets cleared from memory."
6.3 Python Agent with Secrets
#!/usr/bin/env python3
# scripts/run_openclaw_agent.py
import os
import subprocess
from scripts.secrets import load_secrets
# Define which secrets your agent needs
AGENT_SECRETS = {
'TODOIST_API_KEY': 'openclaw/todoist-api-key',
'SLACK_BOT_TOKEN': 'openclaw/slack-token',
'GEMINI_API_KEY': 'openclaw/gemini-key',
}
with load_secrets(AGENT_SECRETS):
# Secrets are now in environment
# Run your OpenClaw agent
subprocess.run(['openclaw', 'status'], check=True)
Part 7: Best Practices Checklist
-
passinstalled on dev machine - GPG key created and passphrase strong
- All secrets stored in
~/.password-store/ - Never use plaintext
.envfiles - Startup scripts fetch secrets at runtime only
- Scripts export secrets as environment variables (not written to files)
- OpenClaw runs within script scope (secrets cleared on exit)
- No secrets in Git history, logs, or version control
- Backup your GPG private key safely (or lose access to all secrets)
Part 8: Troubleshooting
“pass: command not found”
Install it: brew install pass (macOS) or sudo apt-get install pass (Linux)
“gpg: decryption failed”
- Wrong GPG passphrase
- GPG key corrupted (very rare)
- Run:
gpg --list-secret-keysto verify your key exists
“Secret not found”
pass # List all stored secrets
pass todoist/api-key # Verify the exact path
Secrets Still Visible in Process List
Use a more secure approach:
# ✗ Visible: pass as separate process
pass todoist/api-key | xargs -I {} bash -c 'export API_KEY={}; ...'
# ✓ Better: Built into script
secret=$(pass todoist/api-key)
# Secret only in memory, not exposed to other processes
The Real Talk
Using pass means:
- Slightly longer setup (one-time)
- Always remember your GPG passphrase
- Scripts must be in
PATHor explicitly run - But: Zero files with plaintext secrets
It’s the difference between leaving your house with doors locked vs. leaving your keys on the doorstep. Yes, the door is harder to unlock, but nobody’s stealing your keys while you sleep.
Next week: Detecting and auditing which OpenClaw integrations have access to your secrets.