Zero Plaintext Secrets on Disk: Wire Hermes Agent to 1Password with a Survivable Launcher
TL;DR
⚠ Status: Partial solution. This launcher protects keys used directly by Hermes (LLM providers, tool APIs), but Hermes intentionally filters the environment for subprocesses it spawns (MCP servers, gateway platforms like BlueBubbles). Those still need plaintext keys in
.env. For most non-trivial setups, keeping everything in.envis simpler and more reliable.
Replace plaintext API keys in ~/.hermes/.env with op:// secret references. A
tiny launcher script resolves secrets from 1Password at runtime using op read
— nothing sensitive lives on disk. Survives Hermes updates, works in scripts,
cron, and the gateway. No op run required — avoids the environment
stripping that breaks terminal display.
# After setup, your .env goes from this:
DEEPSEEK_API_KEY=sk-abc123def456...
# To this (commented out):
# DEEPSEEK_API_KEY=sk-abc123def456...
# And op.env has only references:
DEEPSEEK_API_KEY=op://AgentShare/Deepseek Agent API key/credential
Why This Matters
Hermes Agent stores API keys in ~/.hermes/.env by default — they sit on disk
in plaintext. If someone reads that file, they have your keys permanently.
Rotating them means hunting down every copy.
1Password stores secrets encrypted at rest. By pointing Hermes at op://
references instead of raw keys, the actual secrets never touch a file. You can
rotate keys in 1Password without touching Hermes’s configuration.
Why Not op run?
The 1Password CLI’s op run command seems like the natural tool — it resolves
op:// references and injects them as environment variables. But in practice,
op run has two problems that break Hermes:
-
Strips environment variables:
op runonly passes through the variables listed in your env-file. It drops$TERM,$LINES,$COLUMNS,$PATH, and terminal control sequences. Hermes’s TUI renders broken and ugly. -
Infinite recursion risk: If the launcher path resolves back to itself, each invocation spawns another, creating an infinite chain.
The fix: Instead of op run, the launcher pre-resolves each secret with
op read, exports them into the current shell environment, then execs the
real Hermes binary. All existing env vars are preserved. No recursion possible.
Step 1: Create a 1Password Service Account
Service accounts let machines authenticate to 1Password without an interactive desktop app.
- Sign in to 1Password
- Go to Settings → Service Accounts
- Click Create Service Account
- Give it a name (e.g. “Hermes Agent”)
- Grant it read access to the vault where your API keys live
- Copy the token — it starts with
ops_
Important: The token is shown exactly once. Save it immediately.
Step 2: Store the Token in Hermes’s Environment
Add the service account token to ~/.hermes/.env so the op CLI can
authenticate:
echo 'OP_SERVICE_ACCOUNT_TOKEN=ops_your_token_here' >> ~/.hermes/.env
No need to put this in
.zshrcor.bashrc. The launcher sources~/.hermes/.envautomatically before resolving secrets — the token stays in one place.
Verify connectivity:
# The launcher will source .env automatically, but you can test manually:
export OP_SERVICE_ACCOUNT_TOKEN="$(grep OP_SERVICE_ACCOUNT_TOKEN ~/.hermes/.env | cut -d= -f2-)"
op whoami
# Should show: User Type: SERVICE_ACCOUNT
Step 3: Install the 1Password CLI
# macOS
brew install 1password-cli
# Linux (Debian/Ubuntu)
curl -sS https://downloads.1password.com/linux/debian/amd64/stable/1password-cli-latest.deb -o /tmp/op.deb
sudo dpkg -i /tmp/op.deb
# Verify
op --version
Step 4: Create API Credential Items in 1Password
For each API key you want to protect, create an API Credential item in your vault:
- Open 1Password
- Navigate to your vault (e.g. “AgentShare”)
- Click New Item → API Credential
- Fill in:
- Title: e.g. “Deepseek Agent API key”
- Credential: paste the actual API key
- Save
Note the secret reference path: op://VaultName/Item Title/credential
Service accounts are read-only by default. If your service account cannot create items, either grant it write access in 1Password admin, use the desktop app to create items manually, or sign in interactively once with
op signin --account my.1password.comto create them from the CLI.
Step 5: Create the Launcher Script
The launcher is a single bash script that pre-resolves secrets with op read,
exports them into the environment, then execs the real Hermes binary. Place it
at ~/.hermes/bin/hermes and symlink it into ~/.local/bin/hermes (ahead of
the real binary in PATH).
# Create the launcher
mkdir -p ~/.hermes/bin
cat > ~/.hermes/bin/hermes << 'SCRIPT'
#!/usr/bin/env bash
# Hermes launcher — resolves 1Password secrets at runtime.
# No plaintext API keys on disk. No op run env mangling.
#
# Uses op read to pre-resolve each secret reference, exports them
# into the current shell, then execs the real Hermes binary.
# All existing env vars ($TERM, $PATH, $HOME, $LINES, $COLUMNS)
# are preserved — the TUI renders normally.
set -euo pipefail
REAL_HERMES="${HERMES_REAL_BIN:-$HOME/.hermes/hermes-agent/venv/bin/hermes}"
OP_ENV_FILE="$HOME/.hermes/op.env"
resolve_secrets() {
local tmpfile
tmpfile=$(mktemp) || return 1
while IFS='=' read -r key ref; do
[[ "$key" =~ ^[[:space:]]*# ]] && continue
[[ -z "$key" ]] && continue
key=$(echo "$key" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
ref=$(echo "$ref" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
value=$(op read "$ref" 2>/dev/null) || {
echo "WARNING: Failed to resolve $ref (key: $key)" >&2
continue
}
printf '%s=%s\n' "$key" "$value"
done < "$OP_ENV_FILE" > "$tmpfile"
echo "$tmpfile"
}
if [ -f "$OP_ENV_FILE" ] && command -v op &>/dev/null; then
SECRETS_FILE=$(resolve_secrets)
trap "rm -f '$SECRETS_FILE'" EXIT
export $(cat "$SECRETS_FILE" | xargs)
fi
exec "$REAL_HERMES" "$@"
SCRIPT
chmod +x ~/.hermes/bin/hermes
# Replace any existing symlink with the new launcher
rm -f ~/.local/bin/hermes
ln -s ~/.hermes/bin/hermes ~/.local/bin/hermes
How it works:
You type: hermes auth list (or any hermes command)
↓
~/.local/bin/hermes → ~/.hermes/bin/hermes (launcher)
↓
op read "op://AgentShare/Deepseek Agent API key/credential"
→ sk-abc123... (resolved from 1Password, never written to disk)
↓
export DEEPSEEK_API_KEY=sk-abc123...
export ELEVENLABS_API_KEY=sk-xyz789...
↓
exec /real/path/to/hermes "$@" ← Hermes sees normal env vars,
$TERM, $PATH, $HOME all intact
The real Hermes binary (~/.hermes/hermes-agent/venv/bin/hermes) is never
touched. hermes update only touches files inside the venv — the launcher
survives.
Step 6: Create Your op.env Mapping File
~/.hermes/op.env maps environment variable names to 1Password references.
One line per key:
cat > ~/.hermes/op.env << 'EOF'
# Hermes 1Password secret mappings — resolved at launch
# Format: ENV_VAR=op://Vault/Item/field
DEEPSEEK_API_KEY=op://AgentShare/Deepseek Agent API key/credential
OPENROUTER_API_KEY=op://AgentShare/OpenRouter API key/credential
ANTHROPIC_API_KEY=op://AgentShare/Anthropic API key/credential
ELEVENLABS_API_KEY=op://AgentShare/Eleven Labs API Key for Agents/credential
# ... add more as you create 1Password items
EOF
This file contains only
op://references — never real keys. Even if someone reads it, they get a pointer, not a secret.
Step 7: Migrate Existing Keys
After creating your 1Password items and op.env, comment out the old plaintext
keys in ~/.hermes/.env:
# Comment out each key (don't delete — keeps a record of what was there)
sed -i '' 's|^DEEPSEEK_API_KEY=|# DEEPSEEK_API_KEY=|' ~/.hermes/.env
sed -i '' 's|^ELEVENLABS_API_KEY=|# ELEVENLABS_API_KEY=|' ~/.hermes/.env
# ... repeat for each key
The Hermes Agent can help with this. Ask it:
“List the API keys currently in plaintext in my .env, and for each one, tell me the exact
op://reference I should add to op.env after I create the corresponding 1Password item.”
Step 8: Test
# Should show your DeepSeek credential resolved from 1Password
hermes auth list
# deepseek (1 credentials):
# #1 DEEPSEEK_API_KEY api_key env:DEEPSEEK_API_KEY ←
# Gateway continues working transparently
hermes gateway status
If you see env:DEEPSEEK_API_KEY with the ← arrow, the injection is
working — the key came from 1Password, not from your .env file.
To double-check that no plaintext remains:
# Should show only the commented-out lines (or no matches)
grep -n 'DEEPSEEK_API_KEY' ~/.hermes/.env
What Survives
| Scenario | Works? |
|---|---|
hermes update |
✓ Launcher at ~/.hermes/bin/, untouched |
| Shell switch (bash → zsh → fish) | ✓ No alias or dotfile dependency |
| Scripts / Makefiles / CI | ✓ ~/.local/bin is in PATH |
| Gateway systemd service | ✓ Unit calls hermes → launcher intercepts |
| Sub-agent delegation | ✓ Non-interactive shells work |
| Changing API keys in 1Password | ✓ No file changes needed — just restart Hermes |
| TUI display | ✓ All terminal env vars preserved |
| MCP subprocesses | ✗ Hermes filters env for MCP servers — keys must be in config.yaml |
| BlueBubbles / gateway services | ✗ Run as separate processes, bypass launcher entirely |
⚠ Important caveat: The launcher only works when you invoke
hermesfrom your shell. Any subprocess that Hermes spawns (MCP servers, platform gateways like BlueBubbles, sub-agents launched viadelegate_task) does NOT inherit the injected secrets. Hermes intentionally filters the environment for these child processes for security reasons. API keys needed by those subprocesses (e.g., MCP server credentials) must still be in~/.hermes/.env.Bottom line: This launcher is a partial solution. It protects keys used directly by Hermes (LLM providers, tool APIs called by the agent itself). Keys consumed by Hermes’s own subprocesses (MCP, gateway platforms) still need to live in
.env. In practice, this limits the approach — most non-trivial Hermes setups end up with keys in both places, defeating the zero-plaintext goal. For now, keeping everything in.env(with 1Password as the source of truth for rotation) is the simpler, more reliable option.
Architecture
~/.local/bin/hermes symlink to launcher (first in PATH)
│
▼
~/.hermes/bin/hermes bash launcher
│
├─ reads ~/.hermes/op.env VAR=op://ref mappings (pointers only)
├─ op read each reference resolved from 1Password at runtime
├─ exports into shell env DEEPSEEK_API_KEY=sk-abc...
│ $TERM, $PATH, $HOME preserved
└─ exec │
▼ │
~/.hermes/hermes-agent/venv/bin/hermes (untouched real binary)
← secrets injected as normal env vars, no op run mangling
Troubleshooting
op CLI not found:
which op # should return a path
Service account can’t read:
op read "op://Vault/Item/field"
# If this fails, check the vault name, item name, and that the service account
# has access to that vault.
Launcher not intercepting:
which hermes # must be ~/.local/bin/hermes
readlink ~/.local/bin/hermes # must point to ~/.hermes/bin/hermes
echo $PATH | tr ':' '\n' | head -3 # ~/.local/bin must be early
Weird TUI display / recursion with old op run approach:
If you previously used the op run-based launcher and see infinite recursion
or broken terminal rendering, check that you’re using the op read-based
launcher from this updated tutorial. The old approach strips terminal
environment variables.
Secret not resolving:
# Test resolution manually
export OP_SERVICE_ACCOUNT_TOKEN="$(grep OP_SERVICE_ACCOUNT_TOKEN ~/.hermes/.env | cut -d= -f2-)"
op read "$(grep 'DEEPSEEK_API_KEY' ~/.hermes/op.env | cut -d= -f2-)"
# Should output your actual API key
Launcher source: GitHub Gist
Comments