MCP API key config security, harness by harness
Aperisa · 9 min read
OpenClaw keeps config backups next to openclaw.json (.bak, then .bak.1 to .bak.4). In issue #76493, an OpenClaw user put the cost this way: "Every config backup (openclaw.json.bak-*) carries a live copy of the bearer, and older backups carry revoked ones." It applies to any harness that keeps the key in its config file.
The fix is the same pattern in every harness: the config holds a reference, and the key lives somewhere else. OpenClaw, Hermes, Cursor and Codex CLI accept an environment reference in remote headers. Hermes and Codex can also pull the key from a vault or a local command. n8n encrypts it as a credential. Our view: an env reference is the minimum. Keep the key in an OS keychain or secret manager and let the harness fetch it at start.
Checked on 2026-10-27 against each vendor's docs and source. If you're new to the protocol, start with what an MCP server is.
Where can each harness read an MCP API key from?
| Harness | Config | Env reference in remote headers | Vault or helper | Known gap |
|---|---|---|---|---|
| OpenClaw | ~/.openclaw/openclaw.json, mcp.servers |
"Bearer ${MCP_TOKEN}" |
SecretRef works for many fields, not mcp.servers |
#76493, open |
| Hermes | ~/.hermes/config.yaml, mcp_servers |
${VAR} or ${env:VAR} |
Bitwarden, 1Password, command helper | Desktop SSH mode reads process env only (#99604) |
| Cursor | ~/.cursor/mcp.json or .cursor/mcp.json |
${env:NAME} |
None documented | envFile is stdio only |
| Codex CLI | ~/.codex/config.toml, mcp_servers |
bearer_token_env_var, env_http_headers |
http_headers_helper |
Helper not for stdio servers |
| n8n | Credentials in n8n's database | Not applicable | External secrets (Enterprise) | Vault values resolve only in credential fields |
OpenClaw: SecretRef doesn't reach mcp.servers yet
OpenClaw SecretRef does not currently work for mcp.servers. Issue #76493 (opened May 3, 2026) reports that mcp.servers[].env and .headers accept only string | number | boolean, so a SecretRef object fails validation. That is still true in 2026.10.9, the latest release (October 29). The fix, PR #69417, was closed unmerged on October 24, 2026, and the SecretRef credential surface lists no mcp.servers path. Use an environment variable in the header, or a wrapper script that loads the key into the Gateway's environment before it starts:
{
mcp: {
servers: {
records: {
url: "https://your-server.example/mcp",
transport: "streamable-http",
headers: { Authorization: "Bearer ${MCP_TOKEN}" },
},
},
},
}
A launchd or systemd Gateway doesn't inherit your shell. The documented fixes are ~/.openclaw/.env, config env, or env.shellEnv.enabled: true. The first two are plaintext files. The third doesn't help here: the shell-env import only brings in a fixed list of expected keys, and an MCP token isn't on it. OpenClaw has no documented keychain path for mcp.servers. The closest is a wrapper that reads the key from your keychain and starts the Gateway with it in the environment. Untested by us. Full config notes are in OpenClaw MCP setup.
Hermes: ${VAR} is confirmed, and it can read from a vault
The Hermes MCP config reference says string values "anywhere in a server entry (env, headers, args, url, …) may reference environment variables with ${VAR} or the Cursor-style SecretRef form ${env:VAR}."
mcp_servers:
records:
url: "https://your-server.example/mcp"
headers:
Authorization: "Bearer ${MCP_RECORDS_API_KEY}"
Values resolve "from the active profile's secret scope (falling back to the process environment)." An unset variable in a remote url or headers fails closed with a named error instead of sending the placeholder. When Hermes saves a Bearer token for you, it writes this template and puts the token in the profile's .env.
The .env file is still plaintext. Hermes's secret sources replace it: Bitwarden and 1Password ship in-tree, and a command helper runs any CLI that prints KEY=VALUE lines. The docs' own example uses secret-tool lookup (GNOME Keyring). Untested by us.
secrets:
command:
enabled: true
command: "secret-tool lookup service hermes-env"
The helper runs once at startup with a 3 second timeout and must be non-interactive, so a keychain that prompts will fail it.
Cursor: ${env:NAME}, from the process Cursor started in
Per the Cursor MCP docs, Cursor "resolves variables in these fields: command, args, env, url, and headers," with ${env:NAME} for environment variables:
{
"mcpServers": {
"records": {
"url": "https://your-server.example/mcp",
"headers": { "Authorization": "Bearer ${env:MCP_TOKEN}" }
}
}
}
"The envFile option is only available for STDIO servers," so a remote server needs the variable in Cursor's own environment. A March 2026 reply from Cursor staff said header interpolation was fixed, and that on Linux an app started from a desktop entry may not see your shell's variables. There, launch Cursor from a terminal.
Codex CLI: an env var name, or a helper command
The Codex MCP docs list three options for a Streamable HTTP server in ~/.codex/config.toml: bearer_token_env_var ("Environment variable name for a bearer token to send in Authorization"), env_http_headers (header names mapped to variable names), and http_headers_helper, a "local command that prints a JSON object of header names and string values."
[mcp_servers.records]
url = "https://your-server.example/mcp"
bearer_token_env_var = "MCP_TOKEN"
The helper is the keychain route. Codex caches its headers per connection and calls it again once after a same-origin 401 or 403. Two limits from the docs: it isn't supported for stdio servers or remote execution environments, and an explicit bearer token or OAuth credential wins over a helper's Authorization header, so don't set both. Codex runs the helper through sh -c with a 10 second timeout. A macOS helper script. Untested by us. We haven't run this helper with Codex; only the security lookup inside it was tested (see the keychain section below). printf doesn't escape JSON, so this only works for a token with no " or \ in it. Use jq -n --arg t "$token" '{Authorization: ("Bearer " + $t)}' if yours might.
# /Users/you/bin/mcp-headers.sh
token=$(security find-generic-password -s mcp-token -w) || exit 1
printf '{"Authorization": "Bearer %s"}\n' "$token"
[mcp_servers.records]
url = "https://your-server.example/mcp"
http_headers_helper = "sh /Users/you/bin/mcp-headers.sh"
For OAuth servers, mcp_oauth_credentials_store can be auto (the default), file or keyring.
n8n: credentials are encrypted, the instance key is what to protect
The MCP Client Tool node takes Bearer, header, multiple-header and OAuth2 credentials. n8n "stores all credentials encrypted in its database," using a key it generates on first launch and saves in ~/.n8n, or one you set with N8N_ENCRYPTION_KEY. Protect that key.
On Enterprise plans, external secrets connect vaults such as 1Password, AWS Secrets Manager and HashiCorp Vault. Vault values "only resolve in credential fields," which is where a Bearer key belongs anyway.
How do you store the key in an OS keychain?
macOS (man security). Tested on macOS 27.0.1. Put -w last so it prompts instead of taking the key on the command line, which would land in shell history. It asks for the value twice:
security add-generic-password -a "$USER" -s mcp-token -w
security find-generic-password -s mcp-token -w
To rotate the key, add -U so the command updates the existing item instead of failing: security add-generic-password -U -a "$USER" -s mcp-token -w. Untested by us. To remove the item: security delete-generic-password -s mcp-token. Tested on macOS 27.0.1.
A keychain keeps the key out of files and history, not out of reach: any process running as your user can read it back with find-generic-password, as our test did without a prompt.
Linux with libsecret (man secret-tool), which reads the value from stdin. Untested by us.
secret-tool store --label="MCP token" service mcp-token
secret-tool lookup service mcp-token
That stores a bare value, fine for a wrapper. The Hermes command helper differs: per its docs, the helper "must print KEY=VALUE lines," and a bare value fails with "helper output was not a KEY=VALUE map." For Hermes, store a dotenv line under the service the helper looks up, and type MCP_RECORDS_API_KEY=<your token> at the prompt. Untested by us.
secret-tool store --label="Hermes env" service hermes-env
For a harness that only reads environment variables, export from the keychain in the shell that starts it. Tested on macOS 27.0.1.
export MCP_TOKEN="$(security find-generic-password -s mcp-token -w)"
For a team, a secret manager adds rotation and audit. Either way, give each agent its own key, and rotate any key that ever sat in a committed file or a config backup.
Where does Aperisa fit?
Aperisa is a remote MCP server. Your data is available to any harness that connects over Streamable HTTP with a Bearer key: OpenClaw, Hermes, Cursor, Codex, Antigravity, n8n or your own code. Give each agent its own key and set what it can reach: which data areas, work or personal, and how much detail, from full records down to totals only. Aperisa enforces those limits on the server.
A reference in config, the key somewhere else
Every harness here can keep the key out of its config file today, and most can keep it out of plaintext entirely. OpenClaw's mcp.servers is the exception: SecretRef doesn't work there while #76493 is open, so use ${VAR}, fed from ~/.openclaw/.env or from a wrapper that reads your keychain before it starts the Gateway. For limiting what each agent can reach once it's connected, see one context layer for many agents and OpenClaw Gmail read-only.
Aperisa connects as a remote MCP server over Streamable HTTP with a Bearer key, and each key can be limited by data area and level of detail. Try it out.
FAQ
Can OpenClaw read an MCP header from a SecretRef?
No. SecretRef does not currently work for mcp.servers. On 2026.10.9, the latest release, the schema for its headers and env accepts only a string, number or boolean, issue #76493 is still open, and the fix in PR #69417 was closed unmerged on October 24, 2026. Use a ${VAR} reference in the header, or a wrapper that loads the key into the Gateway's environment.
Does Hermes support ${VAR} in MCP headers?
Yes. The Hermes MCP config reference says any string in a server entry, including url and headers, can use ${VAR} or ${env:VAR}. Values resolve from the active profile's secret scope, and an unset variable in a remote url or header fails the connection instead of sending the placeholder.
How do I keep a Codex CLI MCP key out of config.toml?
Set bearer_token_env_var to the name of an environment variable, or use http_headers_helper to run a local command that prints the headers as JSON. That command can read the key from your OS keychain.
Is an environment variable enough?
It keeps the key out of the config file and its backups, which is the common leak. It doesn't keep it out of the process environment or a .env file. A keychain or secret manager that the harness reads at start is the stronger setup.
Sources
- OpenClaw issue #76493: SecretRef in mcp.servers env and headers
- OpenClaw PR #69417 (closed, not merged)
- OpenClaw docs: SecretRef credential surface
- OpenClaw docs: Secrets integration examples
- OpenClaw docs: Environment variables
- OpenClaw docs: Env vars FAQ
- Hermes docs: MCP config reference
- Hermes docs: Secrets and command helper source doc
- Hermes source: tools/mcp_tool_config.py and hermes_cli/mcp_config.py
- Hermes issue #99604
- Cursor docs: MCP
- Cursor forum: ${env:NAME} in headers for remote MCP servers
- Codex docs: MCP and sample config
- n8n docs: MCP Client Tool node
- n8n docs: External secrets
- n8n docs: Set a custom encryption key
Questions
Can OpenClaw read an MCP header from a SecretRef?
No. SecretRef does not currently work for mcp.servers. On 2026.10.9, the latest release, the schema for its headers and env accepts only a string, number or boolean, issue #76493 is still open, and the fix in PR #69417 was closed unmerged on October 24, 2026. Use a ${VAR} reference in the header, or a wrapper that loads the key into the Gateway's environment.
Does Hermes support ${VAR} in MCP headers?
Yes. The Hermes MCP config reference says any string in a server entry, including url and headers, can use ${VAR} or ${env:VAR}. Values resolve from the active profile's secret scope, and an unset variable in a remote url or header fails the connection instead of sending the placeholder.
How do I keep a Codex CLI MCP key out of config.toml?
Set bearer_token_env_var to the name of an environment variable, or use http_headers_helper to run a local command that prints the headers as JSON. That command can read the key from your OS keychain.
Is an environment variable enough?
It keeps the key out of the config file and its backups, which is the common leak. It doesn't keep it out of the process environment or a .env file. A keychain or secret manager that the harness reads at start is the stronger setup.
Published
Keep reading
- OpenClaw MCP setup: a remote server with a Bearer header
OpenClaw MCP setup for a remote server with a Bearer header: mcp.servers, not mcpServers or mcporter, an env-backed token, and 2026.6.5 or later.
- n8n MCP Client node: connect a remote MCP server, and fix a missing auth header
Set up the n8n MCP Client and MCP Client Tool nodes against a remote MCP server, and fix the missing Authorization header behind most 401s, step by step.
- Codex CLI and remote MCP servers: running unattended with a Bearer key
Connect Codex CLI 0.162 to a remote MCP server with a Bearer key in config.toml, then make codex exec call its tools unattended without turning off the sandbox.