Agents need credentials to interact with external services — Telegram bot tokens, Notion API keys, Salesforce credentials, webhook secrets. HQ’s secrets system stores these encrypted in the database and delivers them to gateways as environment variables, without the values ever appearing in logs, API responses, or LLM context.Documentation Index
Fetch the complete documentation index at: https://docs.yourhq.ai/llms.txt
Use this file to discover all available pages before exploring further.
How it works
Categories
| Category | Created by | Example |
|---|---|---|
user | Operator, via the Secrets UI | SALESFORCE_API_KEY, STRIPE_WEBHOOK_SECRET |
channel | Automatically at agent creation | TELEGRAM_BOT_TOKEN, DISCORD_BOT_TOKEN, SLACK_APP_TOKEN |
integration | Automatically by OAuth flows | NOTION_SOURCE_A1B2C3D4 (Notion access token) |
Scoping
Secrets are scoped at two levels:- Gateway-scoped (
agent_id = NULL): available to every agent on the gateway. Shown under “Available to all agents” in the UI. - Agent-scoped (
agent_idset): available only to that specific agent. Shown under “Only for this agent.”
.env file is generated by merging gateway defaults with agent overrides.
Managing secrets in the UI
Settings → Secrets
The workspace-level secrets page. Here you manage credentials that all agents on a gateway can use.- Click Add secret.
- Enter a human-readable name (e.g. “Notion API Key”) — the variable name is auto-derived (
NOTION_API_KEY). - Paste the value. It’s encrypted immediately on save.
- Optionally add a note for your future self.
Agent → Secrets tab
Each agent’s detail page has a Secrets tab showing:- Agent-specific secrets — credentials only this agent uses.
- Shared from Settings — gateway-level secrets this agent inherits, with the option to override with an agent-specific value.
Using secrets in agent tools
Agent skills and scripts read secrets from the environment. Thehq_base.py module (loaded by every HQ skill) automatically sources the agent’s .env file at import time:
Encryption details
Algorithm
AES-256-GCM (authenticated encryption with associated data). Ciphertext format:- IV: 12 bytes, randomly generated per encryption
- Tag: 16 bytes (GCM authentication tag)
- Ciphertext: variable length
Key derivation
Self-hosted: The 32-byte encryption key is derived fromSUPABASE_SERVICE_ROLE_KEY using HKDF-SHA256:
- Salt:
yourhq-secrets-v1(fixed) - Info:
aes-256-gcm(fixed) - Output: 32 bytes
HOSTED_SECRETS_KEY directly (32 bytes, base64url-encoded in the worker environment). Independent from the service role key.
Cross-language interoperability
The same encryption format is used by:- Node.js (UI server actions):
apps/ui/src/lib/secrets/crypto.ts - Python (gateway decryption):
gateway/daemons/secrets_sync.py
enc:v1: format, ensuring secrets encrypted by the UI can be decrypted by the gateway.
Gateway sync protocol
Thesecrets_sync daemon runs as a background thread inside the runner container:
- On boot: performs one synchronous sync before the runner processes any commands.
- On Realtime event: any INSERT, UPDATE, or DELETE on the
secretstable (filtered bygateway_id) triggers an immediate re-sync. - Every 5 minutes: safety re-sync catches anything Realtime may have missed.
- Fetches all secrets for this gateway (encrypted values included — uses service role).
- Resolves agent UUIDs to slugs.
- Decrypts each value in memory.
- Writes
~/.openclaw/secrets/gateway.envwith gateway-scoped keys. - Writes
~/.openclaw/secrets/agents/<slug>.envfor each agent (merged: gateway defaults + agent overrides). - Removes stale
.envfiles for agents that no longer have secrets. - PATCHes
sync_status = 'active'andlast_synced_aton successfully synced rows.
chmod 0600; directories with chmod 0700.
A threading.Lock prevents concurrent syncs (Realtime event + periodic timer) from corrupting files.
Channel tokens
When you create an agent with a messaging channel, the wizard writes the channel token to thesecrets table:
| Channel | Secret key | Category |
|---|---|---|
| Telegram | TELEGRAM_BOT_TOKEN | channel |
| Discord | DISCORD_BOT_TOKEN | channel |
| Slack | SLACK_APP_TOKEN, SLACK_BOT_TOKEN | channel |
add-agent.sh reads them from the per-agent .env file after secrets_sync has written it.
You can rotate a channel token from the agent’s Secrets tab — update the value, and the gateway picks up the new token on the next sync cycle (within seconds).
Source connection tokens
OAuth integrations (e.g. Notion) store their access tokens as encrypted secrets:- Key pattern:
{PROVIDER}_SOURCE_{connection_id[:8].upper()}(e.g.NOTION_SOURCE_A1B2C3D4) - Category:
integration - Created automatically by the OAuth callback
source_sync daemon reads the token from the gateway secrets .env file on each sync cycle. If the secret isn’t available (e.g. no gateway configured), it falls back to source_connections.credentials.api_key.
Troubleshooting
Secret shows “Updating…” but never becomes Active
The gateway’ssecrets_sync daemon hasn’t picked up the change. Check:
- Is the runner container running? (
docker compose logs runner) - Does the runner have Supabase Realtime connectivity? Look for WebSocket errors in logs.
- Is the gateway registered with the correct
gateway_id?
Agent doesn’t see the secret
- Verify the secret’s scope — is it gateway-wide or scoped to a different agent?
- Check the
.envfile exists:docker compose exec gateway cat ~/.openclaw/secrets/agents/<slug>.env - If using a custom skill, ensure it imports
hq_base(which sources the.envfile) or reads fromos.environafter the file has been sourced.
Decryption errors in runner logs
- Service role key was rotated without re-encrypting secrets.
HOSTED_SECRETS_KEYwas changed in the worker but old ciphertexts remain in the database.- Database was restored from a backup taken with a different key.
Rotating the service role key
If you rotateSUPABASE_SERVICE_ROLE_KEY:
- All existing secrets become undecryptable (they were encrypted with the old derived key).
- The UI will still work (it encrypts with the new key on write).
- Old secrets will show decryption errors on the gateway.

