My AI Forgot My Passwords (On Purpose)
I run a self-hosted AI agent on my homelab. It reboots crashed containers, monitors uptime, sends me WhatsApp messages, and even claims my free coffee. It needs API keys and passwords to do all of this. It's only been running for a couple of weeks, but I caught myself thinking: "Wait, where are all those passwords actually stored?" The answer was not great. Here's what I did about it.
The Problem With Plaintext
OpenClaw is a self-hosted AI agent running on a Proxmox LXC in my homelab. It has headless Chromium for browser automation and a WhatsApp channel to message me. It talks to Overseerr, Uptime Kuma, Proxmox, and half a dozen other services -- each requiring API keys or credentials.
Out of the box, those credentials lived in all the usual places:
- Environment variables in systemd unit files
- API keys in
openclaw.jsonconfig - Passwords in shell scripts
- Tokens in
auth-profiles.json
If you've ever self-hosted anything, you know the drill. Every service stores its secrets slightly differently and none of them are encrypted. It's the homelab way. But most homelab services don't have an AI agent sitting next to them that can run arbitrary shell commands. Once I thought about it like that, it felt worth sorting out sooner rather than later. Not because anything had gone wrong -- but because "I'll fix it after the incident" is a terrible security strategy.
The Original ClosedClaw
Fortunately, someone had already thought about this problem. ClosedClaw is an open-source encrypted credential vault built specifically for OpenClaw. AES-256-GCM encryption, scrypt key derivation -- proper cryptographic foundations. Credentials go in encrypted, come out only when needed. Perfect.
Well, almost. The original architecture was a daemon. As in, a permanently running HTTP server on port 3847 that sat between your applications and OpenClaw, proxying every request and injecting credentials at runtime:
App --> ClosedClaw daemon (:3847) --> OpenClaw gateway
So to protect my credentials from unnecessary exposure, the solution was... an always-on HTTP server with an open port, handling every single request, running 24/7 as another process that could crash at 3am. The cryptography was solid. The architecture was like hiring a security guard who insists on leaving all the doors open so he can see better.
Ripping Out the Daemon
I forked ClosedClaw and performed what I'll generously call "surgery." I deleted the entire daemon. The HTTP server, the proxy layer, the open port -- all of it. Gone. ClosedClaw became a pure CLI tool that does one thing: decrypt a credential and hand it over.
# Get a single credential
closedclaw get overseerr-api-key --passphrase-file /root/.closedclaw/passphrase
# Returns raw value to stdout -- nothing else
The vault sits encrypted on disk. When a credential is needed, ClosedClaw decrypts it, spits it out, and exits. No process left running, no port left open, no security guard propping the doors open. The credential lives in memory for the duration of the script that requested it, then it's gone. Like it was never there.
Security Hardening
While I had the bonnet up, I added a few things the original was missing:
Brute force protection. After 5 failed unlock attempts, ClosedClaw enforces exponential backoff -- 30 seconds, then 60, 120, 240, up to 5 minutes. If something decides to brute-force the passphrase, it's going to have a very boring afternoon.
Audit trail. Every credential access gets logged to an append-only NDJSON file with timestamps. It's the security equivalent of CCTV -- you might not check it every day, but when something weird happens at 3am, you'll be glad it was recording.
closedclaw audit -n 5
{"timestamp":"2026-03-10T05:02:45Z","action":"credential_get","provider":"octopus-email"}
{"timestamp":"2026-03-10T05:02:45Z","action":"credential_get","provider":"octopus-password"}
{"timestamp":"2026-03-10T05:02:45Z","action":"credential_get","provider":"octopus-account"}
{"timestamp":"2026-03-10T05:02:46Z","action":"credential_get","provider":"octopus-email"}
{"timestamp":"2026-03-10T05:02:46Z","action":"credential_get","provider":"octopus-account"}
Passphrase file validation. The --passphrase-file option is properly paranoid. It rejects symlinks (prevents substitution attacks), checks file ownership (must be the current user), and validates permissions (rejects anything with group or other access). If the file is mode 0644 instead of 0600, ClosedClaw refuses to touch it. Politely, but firmly.
Timing-safe comparison. Hash comparisons use timingSafeEqual() to prevent timing attacks. A wrong passphrase takes the same time to reject as a nearly-right one. No information leaks, even to a patient attacker with a stopwatch.
Generic errors. The get command returns "Authentication failed" whether the passphrase is wrong or the credential doesn't exist. You don't get to fish for what's in the vault by trying different names.
How OpenClaw Uses It
ClosedClaw integrates with OpenClaw through three patterns. Think of them as different levels of "how much effort am I willing to put in?"
Pattern 1: Exec Provider (the fancy one)
For skills that need one API key (like Overseerr), OpenClaw spawns ClosedClaw's exec provider. JSON in, JSON out, no messing about:
# OpenClaw sends:
{"protocolVersion": 1, "ids": ["overseerr-api-key"]}
# ClosedClaw responds:
{"protocolVersion": 1, "values": {"overseerr-api-key": "abc123..."}}
The credential is held in memory, injected as an environment variable for the skill's execution, then cleaned up in a finally block. Never touches disk. The config is just a pointer to the vault:
{
"overseerr": {
"apiKey": {
"source": "exec",
"provider": "closedclaw",
"id": "overseerr-api-key"
}
}
}No plaintext key anywhere. Just "go ask ClosedClaw."
Pattern 2: SystemD EnvironmentFile (the multi-credential one)
Some skills need several credentials at once. Uptime Kuma needs both a username and password, and OpenClaw's config doesn't support multiple SecretRefs per skill. The workaround: a loader script that runs before the gateway starts, reads secrets from the vault, and writes them to a temporary env file:
#!/bin/bash
set -euo pipefail
VAULT="/opt/closedclaw/bin/closedclaw.js"
PF="/root/.closedclaw/passphrase"
ENV="/root/.closedclaw/openclaw-env"
TMPENV=$(mktemp "${ENV}.XXXXXX")
trap 'rm -f "$TMPENV"' EXIT
echo "UPTIME_KUMA_URL=http://192.168.0.33:3001" > "$TMPENV"
echo "UPTIME_KUMA_USERNAME=$(node $VAULT get uptime-kuma-username \
--passphrase-file $PF)" >> "$TMPENV"
echo "UPTIME_KUMA_PASSWORD=$(node $VAULT get uptime-kuma-password \
--passphrase-file $PF)" >> "$TMPENV"
chmod 600 "$TMPENV"
mv "$TMPENV" "$ENV"
trap - EXITAtomic writes (temp file + mv), mode 0600, regenerated fresh from the vault on every service restart. SystemD loads it via EnvironmentFile and the gateway's child processes inherit the credentials without ever knowing the vault exists.
Pattern 3: Direct vault reads (the simple one)
For standalone scripts, just call the vault and get on with your life:
PROXMOX_TOKEN_ID=$(closedclaw get proxmox-token-id --passphrase-file "$VAULT_PF")
PROXMOX_TOKEN_SECRET=$(closedclaw get proxmox-token-secret --passphrase-file "$VAULT_PF")No credentials file on disk. The values exist in the shell session's memory for the duration of the command, then vanish. My Proxmox operations script used to source a plaintext credentials file. Now it asks the vault nicely and forgets everything when it's done.
The Scorecard
Every credential that touches a homelab service is now fully vaulted -- encrypted at rest, decrypted on demand:
- Overseerr API key -- exec provider
- Uptime Kuma username + password -- EnvironmentFile from vault
- Proxmox API token ID + secret -- direct vault reads in pve.sh
- Octopus Energy email, password, account -- direct vault reads in octocoffee.py
- PatchMon API credentials -- file generated from vault
The only credentials still in plaintext are OpenClaw's own internal plumbing -- its LLM API key (auth-profiles.json) and its gateway auth token (systemd Environment=). These don't control any homelab services. They're how OpenClaw talks to its LLM provider and authenticates its own webhook. Both are 0600 permissions and scoped to their individual processes. They can't be vaulted because OpenClaw's runtime manages these files itself and would overwrite any changes on the next update.
So: homelab fully locked down. OpenClaw's own internal wiring is the only thing left, and that's an upstream problem, not a ClosedClaw one. I'll take that score.
The Result
Credentials encrypted at rest with AES-256-GCM. Decrypted on demand, held in memory, cleaned up after use. Every access audited. No daemon, no open ports. The entire thing is a single CLI tool that does one job and exits.
The AI agent gets what it needs, when it needs it, and nothing more. It has genuinely forgotten my passwords -- by design. Which, when you think about it, is exactly what you want from something with shell access to your homelab.
The fork is on GitHub if you want to run it yourself. The original ClosedClaw is there too if you prefer the daemon approach. No judgement. Well, a little judgement.