I Love OpenClaw. I Won't Give It My Keys.
Look, I've been running OpenClaw bots for months. I have one that triages my calendar, one that searches my reading queue, one that watches the home lab and pings me when something's on fire. They're great. They actually do things.
But I do not give them my Google service account key. I do not give them my Brave API token. I do not give them my Sonarr key or my SSH agent socket or my npm publish token. They have none of it. None.
If you prompt-injected one of my bots into running env | grep KEY right now, it would return nothing. There's nothing to find.
This is not because I'm paranoid. It's because the alternative is bad. The Cyera Claw Chain disclosure showed four chainable CVEs that turn a compromised OpenClaw into a persistent backdoor. The Koi Security audit found 341 malicious skills on ClawHub out of 2,857 โ roughly 12% of the marketplace. Any one of those skills, running inside your agent, gets every credential the agent can see.
So I made sure my agents can't see any.
The Pattern: Agent Calls a Proxy. Proxy Holds the Keys.
Here's the idea in one sentence: instead of putting credentials in the agent's environment, run a small proxy service that holds the credentials, and give the agent a tiny CLI that talks to the proxy over a Unix socket.
graph LR
subgraph "Agent (bot user)"
A[OpenClaw
no creds
no chromium]
end
subgraph "Proxy (proxy user)"
B[proxy service
HOLDS THE KEY
validates input]
end
subgraph "External"
C[real API
Google / Brave / ...]
end
A -- "unix socket" --> B
B -- "validated result" --> A
B -- "authenticated request" --> C
The agent runs as one Unix user. The proxy runs as a different Unix user. The credential file is owned by the proxy user and mode 0600 โ the agent literally cannot read it. The two sides talk over a Unix socket that the proxy listens on. The agent's CLI is ten lines of socat and jq.
That's it. That's the whole pattern.
Why I'm Obsessed With This Approach
- Prompt injection gets boring. The most an attacker can extract is "the agent can fetch web pages and read your calendar." That's useful! That's also not "here's the service account key for the whole Google org."
- ClawHub supply chain risk drops to nearly zero. A malicious skill running in the agent finds an empty environment for secrets. It can still abuse the proxy's interface โ but only within whatever the proxy was already willing to do.
- Your audit perimeter shrinks. You audit the proxy. The proxy is fifty lines of shell. You don't audit OpenClaw's 430K lines of Node.
- OpenClaw updates get safer. When the next Claw Chain drops, you update OpenClaw. Your credentials don't move because they never lived there.
- You can swap agents. The same proxy works for OpenClaw, Claude Code, Cursor, Aider, or a shell script. The interface is "Unix socket that takes JSON." Every agent on earth can speak that.
A Concrete Example: Web Fetch
๐ This is the simplest worked example. Takes about 30 minutes to build the first time.
OpenClaw's built-in web_fetch is raw HTTP. No JavaScript. Modern sites return empty shells. So you want a headless browser.
The naive approach: install Chromium in the agent's environment. Now the agent has a real browser, full network egress, and a sandbox-escape primitive sitting in $PATH. If something goes wrong, that's a very bad day.
The proxy approach: Chromium lives in a separate service, behind a Unix socket.
The proxy (runs as user browser-fetch, has Chromium):
#!/usr/bin/env bash
# Listens on /run/browser-fetch-proxy/fetch.sock
# Reads JSON {"url": "..."} from stdin
# Validates the URL
# Renders with headless Chromium
# Cleans with readability-cli, converts with pandoc
# Returns JSON {"content": "...markdown..."}
read -r REQUEST
URL=$(jq -r '.url' <<< "$REQUEST")
# Reject file://, javascript://, RFC 1918, localhost
case "$URL" in
http://*|https://*) ;;
*) echo '{"error":"bad scheme"}'; exit 0 ;;
esac
HOST=$(sed -E 's|^https?://([^/:]+).*|\1|' <<< "$URL")
case "$HOST" in
localhost|127.*|10.*|192.168.*|169.254.*)
echo '{"error":"private network blocked"}'; exit 0 ;;
esac
HTML=$(timeout 45s chromium --headless --no-sandbox --dump-dom "$URL" 2>/dev/null)
CLEAN=$(readable --quiet --html <<< "$HTML")
MD=$(pandoc -f html -t markdown --wrap=none <<< "$CLEAN")
jq -nc --arg c "$MD" '{content: $c}'
Wrap that in a socat UNIX-LISTEN wrapper and you have a service.
The client (runs as the bot user, no Chromium, no anything):
#!/usr/bin/env bash
# browser-fetch <url>
URL="$1"
jq -nc --arg url "$URL" '{"url": $url}' \
| socat -t60 - UNIX-CONNECT:/run/browser-fetch-proxy/fetch.sock \
| jq -r '.content'
That's the entire client. The agent gets browser-fetch <url> in its allowlist. The agent does not get chromium. The agent does not get curl. The agent does not get a way to reach 169.254.169.254 or your home router or any RFC 1918 address.
If the agent goes rogue, the worst it can do via this tool is fetch public web pages. Which it was already allowed to do. The damage cap is "exactly what the proxy was already willing to do for you."
The Same Pattern, With Real Credentials
The browser-fetch case is convincing but mild โ Chromium isn't a credential. Where this pattern really earns its rent is API keys.
For my Google integration, the proxy:
- Owns the service account JSON at
/var/lib/google-proxy/sa-key.json, mode 0600, owned by usergoogle-proxy - Signs JWTs and trades them for OAuth2 access tokens
- Caches tokens in
/run/(tmpfs, never touches disk) with 1-hour expiry - Exposes commands like
calendar today,drive list,gmail search - Refuses anything outside that list
The agent's google-api CLI is the same socat-and-jq shape as browser-fetch. The agent never sees the SA key. The agent never sees the OAuth token. If an attacker convinces the agent to dump ~/.config, they find an empty directory. If they convince it to dump env, they find HOME and PATH and nothing useful.
The actual capability โ reading my calendar, listing files in Drive โ survives the prompt injection. But the credential doesn't move. That's the trade I want every time.
What Makes a Good Proxy vs. a Bad One
Good:
- Runs as a dedicated Unix user, not root, not the agent's user
- Listens on a Unix socket, not TCP, not
localhost:port - Owns the credential file at mode 0600 in a directory the agent can't traverse
- Validates every input before using it โ URL scheme, allowed paths, allowed subcommands
- Returns data only, never echoes the credential in error messages
- Has its own systemd hardening (read-only filesystem, dropped capabilities, restricted syscalls)
- Logs what it does so you can audit it later
Bad:
- Echoes "auth failed, token was
ya29.A0..." in error output - Accepts arbitrary subcommands as passthrough (it's now just a fancier shell)
- Caches tokens on disk in a path the agent user can read
- Runs as root because "it was easier"
- Listens on TCP because "I might want to call it from another machine someday"
The last one is the most common mistake. If the socket is TCP, anything on the box can talk to it. The whole point of the Unix-socket-with-ownership setup is that the kernel enforces the boundary for you, for free.
When to Build a Proxy
Build one any time the agent would need:
- A long-lived credential โ API key, OAuth refresh token, service account JSON, SSH key
- A powerful tool โ headless browser, package manager,
kubectl,terraform,docker - Outbound network to a specific third-party API
- Access to a path on disk that contains anything sensitive
Skip the proxy when:
- The capability is genuinely public and idempotent (e.g. a public search API with no auth and no rate limits worth protecting)
- The capability is a pure stateless transformation on data the agent already has
Rule of thumb: if losing the credential would be bad, put the credential behind a proxy.
Do This Today โก
Two steps. Takes a weekend.
- List every credential your OpenClaw can see right now. Look at its systemd unit's
Environment=, its EnvironmentFile, any.envin its workspace, any OAuth tokens cached in its state directory. Write the list down. - For each credential, write a 50-line proxy script. Read JSON in, validate, call the real API, return JSON out. Wire it to its own systemd user with its own state directory. Replace the agent's direct API access with a CLI that talks to the proxy socket.
That's it. The agent stays smart. The keys move out of reach. The next Claw Chain disclosure becomes a routine OpenClaw update instead of a credential rotation emergency.
If you run NixOS, I have a companion post with the actual modules I use to make this declarative โ per-bot users, hardening attrs, build-time config validation, the works. If you don't run NixOS, the pattern still applies; the implementation is just a few useradd commands and a systemd unit file.
This pairs nicely with removing tools your agent doesn't need. The two ideas reinforce each other: a curated toolbox plus a curated set of credentials. The agent stays useful. The blast radius stays small.
Good enough security that ships > perfect security you never deploy. Build the proxy.
Header image by Kristina Flour on Unsplash
Content on this blog was created using human and AI-assisted workflows described here. Original ideas and editorial decisions by Justin Quaintance.