Skip to main content

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.

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.

How it works

┌─────────────────────────────────────────────────────────────────┐
│  User types value in UI (Settings → Secrets or Agent → Secrets) │
└─────────────────────────┬───────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│  Server action encrypts with AES-256-GCM                        │
│  Stores ciphertext in `secrets` table (value never in plaintext)│
└─────────────────────────┬───────────────────────────────────────┘
                          │  Supabase Realtime

┌─────────────────────────────────────────────────────────────────┐
│  secrets_sync daemon (on gateway)                                │
│  • Decrypts in memory                                           │
│  • Writes ~/.openclaw/secrets/gateway.env (gateway-scoped)       │
│  • Writes ~/.openclaw/secrets/agents/<slug>.env (per-agent)      │
│  • Marks secret as sync_status = 'active'                       │
└─────────────────────────┬───────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│  Agent tool reads os.environ.get("SECRET_KEY")                   │
└─────────────────────────────────────────────────────────────────┘
The value is never in: API responses, command payloads, audit logs, LLM context, git history, or stdout/stderr.

Categories

CategoryCreated byExample
userOperator, via the Secrets UISALESFORCE_API_KEY, STRIPE_WEBHOOK_SECRET
channelAutomatically at agent creationTELEGRAM_BOT_TOKEN, DISCORD_BOT_TOKEN, SLACK_APP_TOKEN
integrationAutomatically by OAuth flowsNOTION_SOURCE_A1B2C3D4 (Notion access token)
All three categories use the same encryption and sync mechanisms. The distinction is for display and management — channel and integration secrets are auto-created and show their origin in the UI.

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_id set): available only to that specific agent. Shown under “Only for this agent.”
When both exist with the same key, the agent-scoped value wins. The per-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.
  1. Click Add secret.
  2. Enter a human-readable name (e.g. “Notion API Key”) — the variable name is auto-derived (NOTION_API_KEY).
  3. Paste the value. It’s encrypted immediately on save.
  4. Optionally add a note for your future self.
The list shows sync status for each secret: Active (on the gateway), Updating… (pending sync), or Sync error.

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.
From here you can add agent-scoped secrets, edit values, or remove overrides (the agent falls back to the shared value).

Using secrets in agent tools

Agent skills and scripts read secrets from the environment. The hq_base.py module (loaded by every HQ skill) automatically sources the agent’s .env file at import time:
import os

api_key = os.environ.get("SALESFORCE_API_KEY")
if not api_key:
    print("Error: SALESFORCE_API_KEY not configured")
No special imports or decryption code is needed in agent tools — secrets arrive as standard environment variables.

Encryption details

Algorithm

AES-256-GCM (authenticated encryption with associated data). Ciphertext format:
enc:v1:<iv-base64url>.<tag-base64url>.<ciphertext-base64url>
  • 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 from SUPABASE_SERVICE_ROLE_KEY using HKDF-SHA256:
  • Salt: yourhq-secrets-v1 (fixed)
  • Info: aes-256-gcm (fixed)
  • Output: 32 bytes
This means anyone with your service role key can derive the encryption key. The secrets system protects against database-layer exposure (dumps, SQL injection, unauthorized Supabase dashboard access) but not against service-role-key compromise. Hosted: Uses 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
Both implementations produce and consume the same enc:v1: format, ensuring secrets encrypted by the UI can be decrypted by the gateway.

Gateway sync protocol

The secrets_sync daemon runs as a background thread inside the runner container:
  1. On boot: performs one synchronous sync before the runner processes any commands.
  2. On Realtime event: any INSERT, UPDATE, or DELETE on the secrets table (filtered by gateway_id) triggers an immediate re-sync.
  3. Every 5 minutes: safety re-sync catches anything Realtime may have missed.
Each sync:
  1. Fetches all secrets for this gateway (encrypted values included — uses service role).
  2. Resolves agent UUIDs to slugs.
  3. Decrypts each value in memory.
  4. Writes ~/.openclaw/secrets/gateway.env with gateway-scoped keys.
  5. Writes ~/.openclaw/secrets/agents/<slug>.env for each agent (merged: gateway defaults + agent overrides).
  6. Removes stale .env files for agents that no longer have secrets.
  7. PATCHes sync_status = 'active' and last_synced_at on successfully synced rows.
All files are written with 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 the secrets table:
ChannelSecret keyCategory
TelegramTELEGRAM_BOT_TOKENchannel
DiscordDISCORD_BOT_TOKENchannel
SlackSLACK_APP_TOKEN, SLACK_BOT_TOKENchannel
The provision command payload does not carry tokens. Instead, 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
The 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’s secrets_sync daemon hasn’t picked up the change. Check:
  1. Is the runner container running? (docker compose logs runner)
  2. Does the runner have Supabase Realtime connectivity? Look for WebSocket errors in logs.
  3. Is the gateway registered with the correct gateway_id?
The 5-minute safety re-sync will eventually catch it, but if Realtime is broken, investigate the connection.

Agent doesn’t see the secret

  1. Verify the secret’s scope — is it gateway-wide or scoped to a different agent?
  2. Check the .env file exists: docker compose exec gateway cat ~/.openclaw/secrets/agents/<slug>.env
  3. If using a custom skill, ensure it imports hq_base (which sources the .env file) or reads from os.environ after the file has been sourced.

Decryption errors in runner logs

[secrets_sync] Decryption failed: ...
This usually means the encryption key doesn’t match. Causes:
  • Service role key was rotated without re-encrypting secrets.
  • HOSTED_SECRETS_KEY was changed in the worker but old ciphertexts remain in the database.
  • Database was restored from a backup taken with a different key.
Fix: re-save each affected secret from the UI (the server action will re-encrypt with the current key).

Rotating the service role key

If you rotate SUPABASE_SERVICE_ROLE_KEY:
  1. All existing secrets become undecryptable (they were encrypted with the old derived key).
  2. The UI will still work (it encrypts with the new key on write).
  3. Old secrets will show decryption errors on the gateway.
To fix: open each secret in the UI, enter a new value, and save. There is no bulk re-encryption tool yet.