Skip to main content

Gateway configuration reference

WednesdayAI reads its configuration from ~/.openclaw/openclaw.json at startup. The file uses JSON5 syntax: standard JSON plus // comments and trailing commas. The config object is strict - unknown top-level keys cause the gateway to refuse to start. The only root-level exception is $schema (a string). If the file does not exist, the gateway starts with safe defaults. Run openclaw doctor to identify and remove unknown keys.

Editing configuration

MethodUse when
openclaw onboard / openclaw configureInteractive wizard for guided setup
openclaw config get|set|unsetOne-off CLI changes
Control panel Config tab (http://localhost:18789/)Browser-based editing with a raw JSON escape hatch
Direct file editScripted or bulk changes - the gateway hot-reloads automatically

Validation and repair

openclaw config validate     # validate against the schema without starting the gateway
openclaw doctor              # identify unknown keys, invalid values
openclaw doctor --fix        # apply repairs, remove unknown keys (backs up first)
When validation fails, only openclaw doctor, openclaw logs, openclaw health, and openclaw status work until the config is fixed.

Hot reload

The gateway watches the config file and applies most changes without a restart:
gateway.reload.modeBehaviour
"hybrid"Hot-apply safe changes; auto-restart for server-level changes (default)
"hot"Hot-apply only; logs a warning when a restart is needed
"restart"Restart on any config change
"off"Manual restarts only
Hot-reload without restart: channels.*, agents.*, models, bindings, hooks, cron, session, messages, tools, browser, skills, audio, logging, ui. Requires restart: gateway.* (port, bind address, auth, TLS, tailscale), discovery, canvasHost, plugins.
{
  gateway: {
    reload: { mode: "hybrid", debounceMs: 300 },
  },
}

Environment variables

The gateway reads env vars from the parent process, .env in the working directory, and ~/.openclaw/.env (neither file overrides existing env vars). Env var substitution in config values via ${VAR_NAME}:
  • Only uppercase names matched: [A-Z_][A-Z0-9_]*
  • Missing or empty vars throw an error at load time
  • Escape with $${VAR} for literal ${VAR} output
Inline env vars and shell import:
{
  env: {
    OPENROUTER_API_KEY: "sk-or-...",
    vars: { GROQ_API_KEY: "gsk-..." },
    shellEnv: { enabled: true, timeoutMs: 15000 },   // or OPENCLAW_LOAD_SHELL_ENV=1
  },
}

Split config with $include

{
  gateway: { port: 18789 },
  agents: { $include: "./agents.json5" },
  broadcast: { $include: ["./clients/a.json5", "./clients/b.json5"] },
}
Single file replaces the containing object; an array deep-merges in order (later wins). Sibling keys merge after includes. Nested includes are supported up to 10 levels deep; relative paths resolve against the including file.

Root keys at a glance

The strict root object accepts these keys (everything else is rejected). Each has its own section or is summarised below.
KeyPurpose
gatewayServer: port, bind, auth, TLS, HTTP endpoints, reload, remote
agentsAgent defaults, model selection, per-agent list
modelsProvider/model catalogue, aliases, fallbacks
channelsMessaging platform connections
bindingsMulti-agent routing rules
broadcastPer-peer broadcast agent fan-out
toolsAgent tool policy (profiles, allow/deny, exec, fs)
approvalsExec-approval forwarding
commandsSlash-command gating (native, bash, config, debug)
messagesInbound/outbound message behaviour, ack reactions, TTS
sessionSession scope, thread bindings, reset policy
cronScheduled job runner
hooksIncoming webhooks + internal lifecycle hooks
pluginsExtension enable/deny, load paths, per-plugin config
skillsSkill loading and limits
memoryMemory backend and citation behaviour
secretsSecretRef providers and resolution
authProvider auth profiles, order, cooldowns
talkText-to-speech provider configuration
audioInbound audio transcription
mediaMedia handling (filename preservation)
browserHeadless/attached browser control
acpAgent Client Protocol bridge
updateUpdate channel and auto-update policy
discoverymDNS and wide-area discovery
canvasHostLocal canvas static host
nodeHostNode.js host configuration including browser proxy settings
webWhatsApp-web client tuning
loggingLog levels, file, redaction
diagnosticsOpenTelemetry export, cache tracing
uiControl-panel branding
cliCLI banner styling
metaBookkeeping (last-touched version/time)
wizardOnboarding wizard bookkeeping
envInline env vars and shell import
networkGlobal outbound SSRF policy (all requests, not just browser)

network - Global outbound SSRF policy

network.ssrfPolicy applies to all outbound HTTP requests made by the gateway — agent tool calls, webhook deliveries, plugin fetches, and so on. browser.ssrfPolicy is a parallel control scoped only to browser-driven fetches; both share the same shape.
{
  network: {
    ssrfPolicy: {
      dangerouslyAllowPrivateNetwork: false,  // allow RFC-1918 / loopback targets
      hostnameAllowlist: ["example.com"],      // always-allowed hostnames
      allowedCidrs: ["10.0.0.0/8"],           // CIDR ranges to allow
      allowRfc2544BenchmarkRange: false,       // allow RFC-2544 benchmark ranges
    },
  },
}
KeyTypeDefaultDescription
network.ssrfPolicy.dangerouslyAllowPrivateNetworkbooleanfalseAllow outbound requests to RFC-1918 private and loopback addresses
network.ssrfPolicy.hostnameAllowliststring[][]Hostnames unconditionally permitted regardless of other policy
network.ssrfPolicy.allowedCidrsstring[][]CIDR ranges to allow
network.ssrfPolicy.allowRfc2544BenchmarkRangebooleanfalseAllow RFC-2544 benchmark address ranges
network.ssrfPolicy supports two field aliases: allowPrivateNetwork (alias for dangerouslyAllowPrivateNetwork) and allowedHostnames (alias for hostnameAllowlist). Prefer the canonical names in new configs.
For browser-specific SSRF controls see browser.ssrfPolicy. The two policies are evaluated independently — a request that passes network.ssrfPolicy but originates from the browser is also checked against browser.ssrfPolicy.

nodeHost — Browser-proxy settings

nodeHost — object — Browser-proxy configuration for this node. Shape: { browserProxy?: { enabled?: boolean, allowProfiles?: string[] } }

gateway - Server settings

{
  gateway: {
    port: 18789,                  // restart required
    mode: "local",                // local | remote (restart required)
    bind: "loopback",             // auto | lan | loopback | tailnet | custom (restart required)
    customBindHost: "10.0.0.5",   // used when bind = "custom"
    auth: {
      mode: "token",              // none | token | password | trusted-proxy
      token: "...",               // or OPENCLAW_GATEWAY_TOKEN
      password: "...",            // or OPENCLAW_GATEWAY_PASSWORD
      allowTailscale: false,
      rateLimit: { maxAttempts: 10, windowMs: 60000, lockoutMs: 300000, exemptLoopback: true },
      trustedProxy: {
        userHeader: "x-forwarded-user",      // required in trusted-proxy mode
        requiredHeaders: ["x-forwarded-by"],
        allowUsers: ["alice@example.com"],
      },
    },
    trustedProxies: ["127.0.0.1"],   // reverse-proxy IPs that terminate TLS
    allowRealIpFallback: false,      // accept X-Real-IP when XFF is missing (default fail-closed)
    controlUi: {
      enabled: true,
      basePath: "/",
      root: "/custom-ui",                 // optional string: custom path for the control UI
      allowedOrigins: ["https://panel.example.com"],
      allowInsecureAuth: false,
      dangerouslyAllowHostHeaderOriginFallback: false,  // use Host header as CORS origin fallback
      dangerouslyDisableDeviceAuth: false,              // disable device auth for control UI
    },
    tailscale: { mode: "off", resetOnExit: false },   // off | serve | funnel (restart required)
    reload: { mode: "hybrid", debounceMs: 300 },
    channelHealthCheckMinutes: 5,
    tls: {
      enabled: false,
      autoGenerate: false,
      certPath: "/etc/openclaw/tls/cert.pem",
      keyPath: "/etc/openclaw/tls/key.pem",
      caPath: "/etc/openclaw/tls/ca.pem",
    },
    remote: {
      url: "wss://gateway.example.com",
      transport: "direct",            // ssh | direct
      token: "...",
      password: "...",                // optional: password auth (SecretInputSchema)
      tlsFingerprint: "sha256/...",
      sshTarget: "user@gateway-host",
      sshIdentity: "~/.ssh/id_ed25519",
    },
    http: {
      endpoints: {
        chatCompletions: { enabled: false },
        responses: { enabled: false },
      },
      securityHeaders: { strictTransportSecurity: false },
    },
    tools: { deny: [], allow: [] },   // HTTP /tools/invoke deny/allow overrides
    nodes: {
      browser: { mode: "auto", node: "studio-mac" },   // auto | manual | off
      allowCommands: [],
      denyCommands: [],
    },
  },
}
KeyDefaultRestart?
gateway.port18789Yes
gateway.mode"local"Yes
gateway.bind"loopback"Yes
gateway.auth.mode"token"Yes
gateway.tailscale.mode"off"Yes
gateway.tls.enabledfalseYes
gateway.reload.mode"hybrid"No
gateway.channelHealthCheckMinutesprovider defaultNo
gateway.restartPolicy"drain"No
gateway.restartResiliencesee descriptionNo
Additional restart-policy keys:
{
  gateway: {
    restartPolicy: "drain",
    restartResilience: { mode: "controlled", drainTimeoutMs: 30000, forceMarksRecovery: false },
  },
}
KeyTypeDefaultDescription
gateway.restartPolicystring"drain""drain" | "force" | "cancel" — controls auto-restart behaviour
gateway.restartResilienceobjectGatewayRestartResilienceConfig: mode ("controlled" | "immediate"), drainTimeoutMs (number), forceMarksRecovery (boolean)
Non-loopback bind requires auth. The gateway refuses to start with bind: "lan", "tailnet", or "custom" if no token or password is set. See Gateway authentication.

Control UI (gateway.controlUi)

KeyTypeDefaultDescription
gateway.controlUi.enabledbooleantrueEnable or disable the control UI entirely
gateway.controlUi.basePathstring"/"URL path prefix for the control UI
gateway.controlUi.rootstringOptional path override: serve the control UI at this path instead of basePath
gateway.controlUi.allowedOriginsstring[][]Explicit CORS origin allowlist
gateway.controlUi.allowInsecureAuthbooleanfalseAllow auth over plain HTTP (not just HTTPS)
gateway.controlUi.dangerouslyAllowHostHeaderOriginFallbackbooleanfalseUse the Host header as the CORS origin when no Origin header is present
gateway.controlUi.dangerouslyDisableDeviceAuthbooleanfalseDisable device authentication for the control UI
dangerouslyDisableDeviceAuth: true removes the device-pairing check from the control UI. Only use this in a fully trusted, isolated environment — enabling it on a network-exposed gateway allows unauthenticated access to the admin interface.

HTTP endpoints

Both OpenAI-compatible endpoints are disabled by default and grant full operator access when enabled. See the API reference for request/response details.
{
  gateway: {
    http: {
      endpoints: {
        chatCompletions: { enabled: true },   // POST /v1/chat/completions
        responses: {                          // POST /v1/responses
          enabled: true,
          maxBodyBytes: 5242880,
          maxUrlParts: 16,
          files: {
            allowUrl: true,
            urlAllowlist: ["https://example.com"],
            allowedMimes: ["application/pdf"],
            maxBytes: 10485760,
            maxChars: 200000,
            pdf: { maxPages: 50, maxPixels: 4000000, minTextChars: 16 },
          },
          images: {
            allowUrl: true,
            urlAllowlist: ["https://example.com"],
            maxBytes: 10485760,
          },
        },
      },
      securityHeaders: {
        // Set only for HTTPS origins you control; false disables the header.
        strictTransportSecurity: "max-age=63072000; includeSubDomains",
      },
    },
  },
}

agents - Agent and model configuration

{
  agents: {
    defaults: {
      workspace: "~/.openclaw/workspace",
      model: {
        primary: "anthropic/claude-sonnet-4-6",
        fallbacks: ["openai/gpt-4o"],
      },
      imageModel: { primary: "openai/gpt-image-1" },
      sandbox: { mode: "off", scope: "agent" },   // mode: off | non-main | all
      heartbeat: { every: "0m", target: "last" },
    },
    list: [
      { id: "default", default: true, workspace: "~/.openclaw/workspace" },
    ],
  },
}
Model refs use provider/model format. agents.defaults.model.primary is set by openclaw models set; the image model by openclaw models set-image.

Model shorthand aliases

The following shorthands can be used anywhere a model ref is expected and resolve to their current default:
AliasResolves to
opusanthropic/claude-opus-4-6
sonnetanthropic/claude-sonnet-4-6
gptopenai/gpt-5.2
gpt-miniopenai/gpt-5-mini
geminigoogle/gemini-3-pro-preview
gemini-flashgoogle/gemini-3-flash-preview
Aliases resolve at runtime; the underlying model ID may change with gateway updates. Use the full provider/model ref if you need to pin to a specific version.

agents.defaults.subagents — Subagent limits

KeyTypeRangeDescription
agents.defaults.subagents.maxConcurrentintegerMax subagents running concurrently per parent agent
agents.defaults.subagents.maxSpawnDepthinteger1–5Max nesting depth for spawned subagent chains
agents.defaults.subagents.maxChildrenPerAgentinteger1–20Max total subagents a single agent can spawn in one turn
agents.defaults.subagents.archiveAfterMinutesintegerArchive completed subagent sessions after this many minutes
agents.defaults.subagents.runTimeoutSecondsintegerTimeout for each subagent run
agents.defaults.subagents.announceTimeoutMsintegerTimeout to wait for subagent announce before proceeding
Values outside the validated ranges (1–5 for maxSpawnDepth, 1–20 for maxChildrenPerAgent) cause the gateway to reject the config on startup.

models - Provider/model catalogue

models.bedrockDiscovery — Automatic Bedrock model discovery

{
  models: {
    bedrockDiscovery: {
      enabled: false,
      region: "us-east-1",
      providerFilter: ["anthropic", "amazon"],
      refreshInterval: 3600,
      defaultContextWindow: 200000,
      defaultMaxTokens: 8192,
    },
  },
}
KeyTypeDefaultDescription
models.bedrockDiscovery.enabledbooleanfalseEnable automatic discovery of available Bedrock models
models.bedrockDiscovery.regionstringAWS region to query for available models
models.bedrockDiscovery.providerFilterstring[]Limit discovery to these model providers (e.g. ["anthropic", "amazon"])
models.bedrockDiscovery.refreshIntervalintegerSeconds between rediscovery polls
models.bedrockDiscovery.defaultContextWindowintegerAssumed context window size for discovered models lacking metadata
models.bedrockDiscovery.defaultMaxTokensintegerAssumed max output tokens for discovered models lacking metadata

models.providers.* — Provider and model config

Each provider entry under models.providers follows this shape:
{
  models: {
    providers: {
      "my-provider": {
        baseUrl: "https://api.example.com/v1",
        apiKey: "MY_PROVIDER_API_KEY",
        auth: "api-key",        // api-key | aws-sdk | oauth | token
        api: "openai-completions",
        models: [
          {
            id: "my-model-id",
            name: "My Model",
            reasoning: false,
            input: ["text", "image"],
            contextWindow: 128000,
            maxTokens: 4096,
            cost: { input: 0.000003, output: 0.000015 },
            compat: {
              maxTokensField: "max_tokens",  // max_tokens | max_completion_tokens
              thinkingFormat: "openai",       // openai | zai | qwen
              supportsStrictMode: true,
              supportsReasoningEffort: false,
              requiresToolResultName: false,
              requiresMistralToolIds: false,
            },
          },
        ],
      },
    },
  },
}
KeyTypeDescription
models.providers.<id>.baseUrlstringBase URL for API requests
models.providers.<id>.apiKeystringEnv var name or secret ref for the API key
models.providers.<id>.authstringAuth method: api-key | aws-sdk | oauth | token
models.providers.<id>.apistringAPI type: openai-completions | openai-responses | anthropic-messages | google-generative-ai | bedrock-converse-stream | ollama | github-copilot
models.providers.<id>.models[].idstringProvider-side model ID
models.providers.<id>.models[].namestringDisplay name
models.providers.<id>.models[].reasoningbooleanWhether the model supports extended reasoning
models.providers.<id>.models[].inputstring[]Accepted input types: "text", "image"
models.providers.<id>.models[].contextWindowintegerContext window in tokens
models.providers.<id>.models[].maxTokensintegerMax output tokens
models.providers.<id>.models[].cost.inputnumberCost per input token in USD
models.providers.<id>.models[].cost.outputnumberCost per output token in USD
models.providers.<id>.models[].compat.maxTokensFieldstringField name to use for max tokens: max_tokens | max_completion_tokens
models.providers.<id>.models[].compat.thinkingFormatstringThinking tag format for reasoning models: openai | zai | qwen
models.providers.<id>.models[].compat.supportsStrictModebooleanWhether the model supports strict tool-schema mode
models.providers.<id>.models[].compat.supportsReasoningEffortbooleanWhether the model accepts a reasoning-effort parameter
models.providers.<id>.models[].compat.requiresToolResultNamebooleanWhether tool result messages must include a name field
models.providers.<id>.models[].compat.requiresMistralToolIdsbooleanWhether the Mistral-style tool ID convention is required

tools - Agent tool policy

{
  tools: {
    profile: "messaging",          // minimal | coding | messaging | full
    allow: ["web_search", "image"],
    alsoAllow: ["pdf"],
    deny: ["exec"],
    byProvider: { anthropic: { profile: "full" } },
    sessions: { visibility: "agent" },   // self | tree | agent | all
    message: { allowCrossContextSend: false },
    agentToAgent: { enabled: false, allow: [] },
    elevated: { enabled: false },
    exec: { /* exec tool policy */ },
    fs: { /* filesystem tool policy */ },
  },
}
Set either allow or alsoAllow in a scope, not both. alsoAllow extends the profile; allow replaces it.

approvals - Exec-approval forwarding

{
  approvals: {
    exec: {
      enabled: true,
      mode: "both",                // session | targets | both
      agentFilter: ["main"],
      targets: [
        { channel: "telegram", to: "123456789", accountId: "default" },
      ],
    },
  },
}

commands - Slash-command gating

{
  commands: {
    native: "auto",                // auto | on | off (native channel commands)
    nativeSkills: "auto",
    text: true,                    // text-form /commands
    bash: false,                   // off by default - enables `! ` / `/bash`
    config: true,                  // /config persisted changes
    debug: false,                  // /debug runtime overrides
    restart: true,                 // allow /restart
    ownerDisplay: "raw",           // raw | hash
  },
}
Additional commands keys:
{
  commands: {
    aliases: {
      gm: "greet morning",
    },
    disabled: [],
  },
}
KeyTypeDefaultDescription
commands.aliasesobject{}Custom slash-command aliases mapping alias → real command
commands.disabledstring[][]Command names to disable globally

messages - Message behaviour

{
  messages: {
    messagePrefix: "",
    responsePrefix: "",
    ackReaction: "👀",
    ackReactionScope: "group-mentions",   // group-mentions | group-all | direct | all | off | none
    removeAckAfterReply: false,
    statusReactions: { enabled: true },
    suppressToolErrors: false,
    queue: { /* per-peer queue tuning */ },
    inbound: { /* debounce */ },
    groupChat: { /* group reply rules */ },
    tts: { /* see talk */ },
  },
}

channels - Messaging platform connections

Built-in channels: whatsapp, telegram, discord, slack, signal, imessage, bluebubbles, googlechat, msteams, irc. Additional channels (Matrix, Zalo, Mattermost, Nostr, and others) are provided by extensions and configured under the same channels object.

Common DM policy values

dmPolicyEffect
"pairing"New senders get a pairing code; ignored until approved (default)
"allowlist"Only allowFrom entries can initiate conversations
"open"Any sender (requires allowFrom: ["*"])
"disabled"Agent does not respond to DMs

WhatsApp

{
  channels: {
    whatsapp: {
      enabled: true,
      dmPolicy: "pairing",
      allowFrom: ["+15555550123"],
      groupPolicy: "allowlist",
      groups: { "<jid>@g.us": true },
      textChunkLimit: 4000,
      ackReaction: { emoji: "👀", direct: "always", group: "mentions" },
      accounts: { work: { authDir: "~/.openclaw/credentials/whatsapp/work" } },
    },
  },
}
Login: openclaw channels login --channel whatsapp [--account work]

Telegram

{
  channels: {
    telegram: {
      enabled: true,
      botToken: "123:abc",            // or TELEGRAM_BOT_TOKEN
      dmPolicy: "pairing",
      allowFrom: ["123456789"],
      groupPolicy: "allowlist",
      groups: {
        "*": { requireMention: true },
        "-1001234567890": { requireMention: false, groupPolicy: "open" },
      },
      streaming: "partial",           // off | partial | block | progress
      textChunkLimit: 4000,
    },
  },
}

Discord

{
  channels: {
    discord: {
      enabled: true,
      token: "YOUR_BOT_TOKEN",        // or DISCORD_BOT_TOKEN
      dmPolicy: "pairing",
      groupPolicy: "allowlist",
      guilds: {
        "123456789012345678": {
          requireMention: true,
          users: ["987654321098765432"],
          roles: ["111222333444555666"],
        },
      },
      streaming: "off",
    },
  },
}

Slack

{
  channels: {
    slack: {
      enabled: true,
      mode: "socket",                 // socket | http
      appToken: "xapp-...",           // Socket Mode only
      botToken: "xoxb-...",
      dmPolicy: "pairing",
      groupPolicy: "allowlist",
      channels: { "C0123456789": { requireMention: false, users: ["U0123456789"] } },
      streaming: "partial",
      nativeStreaming: true,
    },
  },
}

Signal

{
  channels: {
    signal: {
      enabled: true,
      account: "+15551234567",
      cliPath: "signal-cli",
      autoStart: true,
      dmPolicy: "pairing",
      allowFrom: ["+15557654321"],
    },
  },
}

iMessage and BlueBubbles

{
  channels: {
    bluebubbles: {                    // recommended modern iMessage path
      enabled: true,
      serverUrl: "http://127.0.0.1:1234",
      password: "...",
      dmPolicy: "pairing",
    },
    imessage: { enabled: false },     // legacy imsg stdio adapter
  },
}

session - Session scope and reset

{
  session: {
    dmScope: "per-channel-peer",      // main | per-peer | per-channel-peer | per-account-channel-peer
    threadBindings: { enabled: true, idleHours: 24, maxAgeHours: 0 },
    reset: { mode: "daily", atHour: 4, idleMinutes: 120 },   // daily | idle
  },
}
Additional session keys:
{
  session: {
    storage: {
      backend: "sqlite",       // "fs-jsonl" | "sqlite" | "postgres"
      sqlitePath: "",          // path to .db file (sqlite only)
      databaseUrl: "",         // connection string (postgres only)
      cache: {
        enabled: true,
        maxEntries: 500,
      },
    },
    maintenance: {
      mode: "auto",              // "auto" | "manual"
      pruneAfter: 90,            // days before pruning
      maxEntries: 0,             // max session entries (0 = no limit)
      rotateBytes: 0,            // rotate session file after this size
      maxDiskBytes: 0,           // cap total disk usage
      highWaterBytes: 0,         // trigger maintenance at this threshold
    },
  },
}
KeyTypeDefaultDescription
session.storage.backendstring"sqlite""fs-jsonl" | "sqlite" | "postgres" — backing store for session data
session.storage.sqlitePathstringPath to the SQLite .db file (sqlite only)
session.storage.databaseUrlstringConnection string (postgres only)
session.storage.cache.enabledbooleantrueEnable in-memory session cache
session.storage.cache.maxEntriesinteger500Max entries in the session cache
session.maintenance.modestring"auto""auto" | "manual"
session.maintenance.pruneAfternumber90Days before pruning old sessions
session.maintenance.maxEntriesinteger0Max session entries (0 = no limit)
session.maintenance.rotateBytesinteger0Rotate session file after this many bytes
session.maintenance.maxDiskBytesinteger0Cap total disk usage in bytes
session.maintenance.highWaterBytesinteger0Trigger maintenance at this threshold

cron - Scheduled jobs

{
  cron: {
    enabled: true,
    maxConcurrentRuns: 2,
    sessionRetention: "24h",          // duration, or false to keep sessions
    retry: { maxAttempts: 3, backoffMs: [1000, 5000], retryOn: ["rate_limit", "network"] },
    runLog: { maxBytes: "2mb", keepLines: 2000 },
    failureAlert: { enabled: true, after: 3, mode: "announce" },   // announce | webhook
    failureDestination: {
      channel: "telegram",
      to: "123456789",
      accountId: "default",
      mode: "announce",
    },
  },
}
Additional cron keys:
{
  cron: {
    webhook: "https://example.com/cron-trigger",   // HttpUrl — plain URL
    webhookToken: "shared-secret",
  },
}
KeyTypeDefaultDescription
cron.storestringOptional named store identifier for cron job persistence
cron.webhookstring (HttpUrl)URL to receive cron trigger notifications
cron.webhookTokenstringAuthentication token for the cron webhook
cron.failureAlert.cooldownMsnumberMinimum milliseconds between repeated failure alerts; prevents alert storms
cron.failureDestinationobjectSeparate routing target for failure alerts: { channel?, to?, accountId?, mode? }. Distinct from cron.failureAlert (which controls triggering); this sets where alerts go

hooks - Incoming webhooks and lifecycle hooks

{
  hooks: {
    enabled: true,
    token: "shared-secret",
    path: "/hooks",
    defaultSessionKey: "hook:ingress",
    allowRequestSessionKey: false,
    allowedSessionKeyPrefixes: ["hook:"],
    maxBodyBytes: 1048576,
    mappings: [
      { match: { path: "gmail" }, action: "agent", agentId: "main", deliver: true },
    ],
    allowedAgentIds: ["main"],          // optional: restrict hook delivery to these agent IDs
    gmail: { /* Gmail hooks integration config */ },
    internal: { enabled: true, entries: { "boot-md": { enabled: true } } },
  },
}
Treat all webhook payload content as untrusted. Keep unsafe-content bypass flags disabled unless debugging.
See the Hooks catalogue for bundled internal hooks. Additional hooks keys:
{
  hooks: {
    presets: [],
    transformsDir: "",
  },
}
KeyTypeDefaultDescription
hooks.presetsstring[][]Preset names to load: "safety", "productivity", "audit"
hooks.transformsDirstringPath to a directory of transform scripts applied to all hook payloads
hooks.allowedAgentIdsstring[]Optional allowlist of agent IDs that can receive hook deliveries

hooks.gmail — Gmail push notification integration

Receives Gmail push notifications via Google Cloud Pub/Sub and routes them as hook events.
KeyTypeDefaultDescription
hooks.gmail.accountstringGmail account address to monitor
hooks.gmail.labelstringGmail label to filter (e.g. "INBOX")
hooks.gmail.topicstringGoogle Cloud Pub/Sub topic name
hooks.gmail.subscriptionstringPub/Sub subscription name
hooks.gmail.pushTokenstringSecret token used to validate incoming Pub/Sub pushes
hooks.gmail.hookUrlstringPublic URL that Google will push notifications to
hooks.gmail.includeBodybooleanfalseWhether to include the email body in the hook payload
hooks.gmail.maxBytesintegerMaximum bytes to include from the email body
hooks.gmail.renewEveryMinutesintegerHow often to renew the Pub/Sub push subscription
hooks.gmail.allowUnsafeExternalContentbooleanfalsePass raw email content to the agent (use with caution)
hooks.gmail.serve.bindstringBind address for the local Pub/Sub push receiver
hooks.gmail.serve.portintegerPort for the local Pub/Sub push receiver
hooks.gmail.serve.pathstringURL path for the local Pub/Sub push receiver
hooks.gmail.tailscale.modestring"off"Expose the receiver via Tailscale: off | serve | funnel
hooks.gmail.modelstringModel to use for agent turns triggered by Gmail events
hooks.gmail.thinkingstring"off"Thinking level: off | minimal | low | medium | high

plugins - Extensions

{
  plugins: {
    enabled: true,
    allow: ["brave-search"],
    deny: [],
    load: { paths: ["~/.openclaw/extensions/my-plugin"] },
    slots: { memory: "my-memory-plugin" },
    entries: { "brave-search": { enabled: true, config: { apiKey: "..." } } },
  },
}
Most plugin changes require a gateway restart. Additional plugins keys:
{
  plugins: {
    installs: {
      "@wednesdayai/voice-openai": { version: "^1.0" },
    },
  },
}
KeyTypeDefaultDescription
plugins.installsobject{}Plugin specifiers auto-installed on gateway start. Keyed by plugin name: { "<plugin-name>": { version } }

skills - Skill loading

{
  skills: {
    allowBundled: ["status", "memory-search"],
    load: { extraDirs: ["~/.openclaw/skills"], watch: true, watchDebounceMs: 300 },
    install: { preferBrew: false, nodeManager: "pnpm" },   // npm | pnpm | yarn | bun
    limits: { maxSkillsInPrompt: 30, maxSkillsPromptChars: 20000 },
    entries: {
      "my-skill": {
        enabled: true,
        apiKey: "...",
        env: { MY_KEY: "value" },           // env vars injected when the skill runs
        config: { theme: "dark" },          // arbitrary skill-specific config
      },
    },
  },
}
Additional skills keys:
{
  skills: {
    limits: {
      maxSkillFileBytes: 524288,
      maxCandidatesPerRoot: 100,
      maxSkillsLoadedPerSource: 50,
    },
  },
}
KeyTypeDefaultDescription
skills.load.watchDebounceMsnumberDebounce delay (ms) before reloading after a skills file change (requires skills.load.watch: true)
skills.limits.maxSkillFileBytesinteger524288Max SKILL.md file size in bytes (512 KB)
skills.limits.maxCandidatesPerRootintegerMax skill candidates per root directory
skills.limits.maxSkillsLoadedPerSourceintegerMax skills loaded per source
skills.entries.<id>.enabledbooleantrueEnable or disable this skill entry
skills.entries.<id>.apiKeystringAPI key passed to the skill
skills.entries.<id>.envRecord<string, string>Environment variables injected when the skill runs
skills.entries.<id>.configRecord<string, unknown>Arbitrary skill-specific configuration object

memory - Memory backend

{
  memory: {
    backend: "builtin",               // builtin | qmd | external
    citations: "auto",                // auto | on | off
    qmd: { searchMode: "search" },    // query | search | vsearch
  },
}

secrets - SecretRef resolution

{
  secrets: {
    providers: {
      "env": { source: "env" },
      "vault": { source: "exec", command: "/usr/local/bin/vault-fetch" },
    },
    defaults: { env: "env", file: "file", exec: "vault" },
    resolution: { maxProviderConcurrency: 4, maxRefsPerProvider: 256 },
  },
}
Sensitive values throughout the config (gateway.auth.token, channels.*.botToken, talk.apiKey, and so on) accept a SecretRef instead of a plaintext string. Run openclaw secrets reload after changing providers.

auth - Provider auth profiles

{
  auth: {
    profiles: {
      "anthropic:work": { provider: "anthropic", mode: "oauth", email: "you@example.com" },
    },
    order: { anthropic: ["anthropic:work", "anthropic:personal"] },
    cooldowns: { billingBackoffHours: 6, failureWindowHours: 1 },
  },
}
Auth profile mode is one of api_key, oauth, or token. Managed via openclaw models auth .... Additional auth keys:
{
  auth: {
    cooldowns: {
      billingBackoffHoursByProvider: { anthropic: 6 },
      billingMaxHours: 24,
    },
  },
}
KeyTypeDefaultDescription
auth.cooldowns.billingBackoffHoursByProviderobject{}Per-provider billing backoff hours (record keyed by provider name)
auth.cooldowns.billingMaxHoursnumberMaximum billing backoff hours

talk and audio - Voice

{
  talk: {
    provider: "elevenlabs",
    voiceId: "...",
    modelId: "...",
    apiKey: "...",
    interruptOnSpeech: true,
    providers: { elevenlabs: { voiceId: "...", apiKey: "..." } },
  },
  audio: {
    transcription: { /* inbound audio transcription settings */ },
  },
}

browser - Browser control

{
  browser: {
    enabled: true,
    evaluateEnabled: false,
    headless: false,
    defaultProfile: "chrome",
    ssrfPolicy: { dangerouslyAllowPrivateNetwork: false, hostnameAllowlist: ["example.com"] },
    snapshotDefaults: { mode: "efficient" },   // controls default snapshot strategy
    // Remote CDP / executable overrides
    cdpUrl: "ws://127.0.0.1:9222/json/version",  // connect to a remote CDP endpoint
    remoteCdpTimeoutMs: 10000,                    // timeout for remote CDP connection
    remoteCdpHandshakeTimeoutMs: 5000,            // timeout for CDP handshake
    executablePath: "/usr/bin/chromium-browser",  // override auto-detected browser binary
    noSandbox: false,                             // disable sandbox (for containers)
    attachOnly: false,                            // attach to existing browser, do not launch
    cdpPortRangeStart: 9222,                      // starting port for CDP port allocation
    extraArgs: ["--disable-gpu"],                 // extra browser launch arguments
    profiles: {
      "work": { cdpPort: 9222, driver: "clawd", color: "#4A90D9" },   // driver: clawd | extension; color: hex for UI distinction
    },
  },
}
Additional browser SSRF policy keys:
{
  browser: {
    ssrfPolicy: {
      dangerouslyAllowPrivateNetwork: false,
      hostnameAllowlist: [],
      allowedCidrs: [],
      allowRfc2544BenchmarkRange: false,
    },
  },
}
KeyTypeDefaultDescription
browser.ssrfPolicy.dangerouslyAllowPrivateNetworkbooleanfalseAllow the browser to fetch RFC-1918 private addresses
browser.ssrfPolicy.hostnameAllowliststring[][]Hostnames to allow
browser.ssrfPolicy.allowedCidrsstring[][]CIDR ranges to allow
browser.ssrfPolicy.allowRfc2544BenchmarkRangebooleanfalseAllow RFC-2544 benchmark address ranges
browser.snapshotDefaults.modestringDefault snapshot strategy for browser tool calls: "efficient"
browser.ssrfPolicy supports two field aliases: allowPrivateNetwork (alias for dangerouslyAllowPrivateNetwork) and allowedHostnames (alias for hostnameAllowlist). Prefer the canonical names in new configs.

Additional browser keys

KeyTypeDefaultDescription
browser.cdpUrlstringConnect to a remote CDP endpoint instead of launching a browser (e.g. ws://127.0.0.1:9222/json/version)
browser.remoteCdpTimeoutMsnumberTimeout (ms) for establishing a remote CDP connection
browser.remoteCdpHandshakeTimeoutMsnumberTimeout (ms) for the CDP handshake after connecting
browser.executablePathstringPath to the browser executable; overrides auto-detection
browser.noSandboxbooleanfalseDisable the browser sandbox (required in some containerized environments)
browser.attachOnlybooleanfalseAttach to an already-running browser instead of launching a new one
browser.cdpPortRangeStartnumberStarting port number for CDP port allocation
browser.extraArgsstring[][]Additional arguments passed to the browser on launch
browser.profiles[*].colorstringHex color (#RRGGBB) used to visually distinguish this profile in the UI

acp - Agent Client Protocol bridge

{
  acp: {
    enabled: true,
    defaultAgent: "main",
    allowedAgents: ["main"],
    maxConcurrentSessions: 4,
    stream: { deliveryMode: "live" },   // live | final_only
  },
}
Additional ACP streaming keys:
{
  acp: {
    stream: {
      coalesceIdleMs: 50,
      maxChunkChars: 2000,
      repeatSuppression: true,
      hiddenBoundarySeparator: "none",    // "none" | "space" | "newline" | "paragraph"
      maxOutputChars: 10000,              // positive integer; omit for unlimited
      maxSessionUpdateChars: 4000,        // positive integer; omit for unlimited
      tagVisibility: { "thinking": false },   // Record<string, boolean>
    },
    dispatch: { /* dispatch configuration */ },
    backend: "custom-backend",            // optional string identifier
    runtime: { /* runtime configuration */ },
  },
}
KeyTypeDefaultDescription
acp.stream.coalesceIdleMsinteger50Idle time before flushing a partial stream chunk (ms)
acp.stream.maxChunkCharsintegerMax characters per stream chunk
acp.stream.repeatSuppressionbooleantrueSuppress repeated identical chunks
acp.stream.hiddenBoundarySeparatorenum"none"Separator at hidden chunk boundaries: "none" | "space" | "newline" | "paragraph"
acp.stream.maxOutputCharsintegerMax total output characters; omit for unlimited (must be positive integer if set)
acp.stream.maxSessionUpdateCharsintegerMax chars per session update; omit for unlimited (must be positive integer if set)
acp.stream.tagVisibilityRecord<string, boolean>Per-tag visibility overrides, e.g. { "thinking": false }
acp.dispatchobjectDispatch sub-object (see extended config)
acp.backendstringOptional string identifier for the backend
acp.runtimeobjectRuntime environment config: { ttlMinutes?: number, installCommand?: string }. ttlMinutes sets the runtime TTL; installCommand is run to install runtime dependencies before the session starts

update - Update policy

{
  update: {
    channel: "stable",                  // stable | beta | dev
    checkOnStart: true,
    auto: {
      enabled: false,
      stableDelayHours: 6,
      stableJitterHours: 12,
      betaCheckIntervalHours: 1,
    },
  },
}
KeyTypeDefaultDescription
update.auto.stableDelayHoursnumber6Hours to delay applying a stable update after release (max 168)
update.auto.stableJitterHoursnumber12Random jitter (hours) added to update checks (max 168)
update.auto.betaCheckIntervalHoursnumber1How often (hours) the beta channel is checked (max 24)

discovery and canvasHost

{
  discovery: {
    mdns: { mode: "minimal" },          // off | minimal | full
    wideArea: { enabled: false },
  },
  canvasHost: {
    enabled: true,
    root: "~/.openclaw/canvas",
    port: 18793,
    liveReload: true,
  },
}

web - WhatsApp Web client

Controls the WhatsApp Web client’s connection behaviour and reconnection strategy.
{
  web: {
    enabled: true,
    heartbeatSeconds: 30,
    reconnect: {
      initialMs: 1000,      // initial reconnect delay
      maxMs: 30000,         // maximum reconnect delay (ms)
      factor: 2,            // exponential backoff multiplier
      jitter: 0.2,          // fractional jitter applied to each delay
      maxAttempts: 0,       // 0 = unlimited retries
    },
  },
}
KeyTypeDefaultDescription
web.enabledbooleantrueEnable or disable the WhatsApp Web client
web.heartbeatSecondsnumberInterval (seconds) between keep-alive heartbeats
web.reconnect.initialMsnumberInitial reconnect delay in milliseconds
web.reconnect.maxMsnumberMaximum reconnect delay in milliseconds
web.reconnect.factornumberExponential backoff multiplier applied after each failed attempt
web.reconnect.jitternumberFractional jitter (0–1) added to each computed delay to spread reconnects
web.reconnect.maxAttemptsnumberMaximum reconnect attempts before giving up (0 = unlimited)

broadcast - Broadcast fan-out

{
  broadcast: {
    "team-peer-id": ["main", "work"],   // agent ids that receive a broadcast
  },
}
Each agent id listed must exist in agents.list.

logging - Log levels and output

{
  logging: {
    level: "info",                      // silent | fatal | error | warn | info | debug | trace
    file: "/tmp/openclaw/openclaw-YYYY-MM-DD.log",
    consoleLevel: "info",
    consoleStyle: "pretty",             // pretty | compact | json
    redactSensitive: "tools",           // off | tools
    redactPatterns: ["sk-.*"],
    maxFileBytes: 10485760,             // rotate log file after 10 MB
    maxBackups: 3,                      // number of rotated log files to keep
    fileLogs: {                         // per-profile file logging
      defaults: { file: "/tmp/openclaw/openclaw.log", maxFileBytes: 10485760, maxBackups: 3 },
      profiles: {
        "verbose": { file: "/tmp/openclaw/verbose.log", maxFileBytes: 5242880, compress: "gzip" },
      },
    },
  },
}
OPENCLAW_LOG_LEVEL=debug overrides both level and consoleLevel.
KeyTypeDefaultDescription
logging.levelstring"info"Global log level: silent | fatal | error | warn | info | debug | trace
logging.filestringPath template for the log file; YYYY-MM-DD is substituted with the current date
logging.consoleLevelstring"info"Log level for console (stdout) output
logging.consoleStylestring"pretty"Console format: pretty | compact | json
logging.redactSensitivestring"tools"Redaction scope: off | tools
logging.redactPatternsstring[][]Additional regex patterns whose matches are redacted from logs
logging.maxFileBytesintegerMax log file size in bytes before rotation (e.g. 10485760 = 10 MB)
logging.maxBackupsintegerNumber of rotated log files to retain
logging.fileLogsobjectPer-profile file logging config. Contains defaults (base log config) and profiles (named overrides) sub-objects

diagnostics - OpenTelemetry export and session capture

{
  diagnostics: {
    enabled: true,
    otel: {
      enabled: true,
      endpoint: "http://otel-collector:4318",
      protocol: "http/protobuf",        // http/protobuf | grpc
      traces: true,
      metrics: true,
      logs: false,
      sampleRate: 1.0,
      tracesEndpoint: "http://otel-collector:4318/v1/traces",   // per-signal override
      metricsEndpoint: "http://otel-collector:4318/v1/metrics",
      logsEndpoint: "http://otel-collector:4318/v1/logs",
      serviceName: "wednesdayai-gateway",
      flushIntervalMs: 5000,
      captureContent: false,            // or object with granular flags
      headers: {},                      // auth/custom headers for OTLP exporter
      langfuse: {
        host: "https://cloud.langfuse.com",
        publicKey: "pk-lf-...",
        secretKey: "sk-lf-...",
        filterInfraSpans: true,
        sessionSpans: true,
        sessionIdleTimeoutMs: 1800000,
      },
      runtime: {
        enabled: false,
        intervalMs: 30000,
        collectProc: false,
      },
      modelPricing: {
        // Record<provider, Record<model, { inputPerMToken, outputPerMToken }>>
        "anthropic": {
          "claude-sonnet-4-6": { inputPerMToken: 3.0, outputPerMToken: 15.0 },
        },
      },
    },
    capture: { mode: "off" },           // off | manual | always | schedule
    cacheTrace: {
      enabled: false,
      filePath: "/tmp/openclaw/cache-trace.jsonl",
      includeMessages: false,
      includePrompt: false,
      includeSystem: false,
    },
    modelPayloadLog: {
      enabled: false,
      usageSummaries: true,
      rawPayloads: false,
      captureContent: false,
      providers: [],
      file: "",
      maxQueuedBytes: 0,
    },
    stuckSessionWarnMs: 60000,
    stuckSessionAbortMs: 300000,
  },
}

diagnostics.otel keys

KeyTypeDefaultDescription
diagnostics.otel.enabledbooleanfalseEnable OTLP export
diagnostics.otel.endpointstringBase OTLP collector endpoint (used when per-signal endpoints are not set)
diagnostics.otel.tracesEndpointstringPer-signal OTLP endpoint for traces; overrides endpoint for traces
diagnostics.otel.metricsEndpointstringPer-signal OTLP endpoint for metrics; overrides endpoint for metrics
diagnostics.otel.logsEndpointstringPer-signal OTLP endpoint for logs; overrides endpoint for logs
diagnostics.otel.protocolstring"http/protobuf""http/protobuf" | "grpc"
diagnostics.otel.tracesbooleantrueExport trace spans
diagnostics.otel.metricsbooleantrueExport metrics
diagnostics.otel.logsbooleanfalseExport logs via OTLP
diagnostics.otel.sampleRatenumber1.0Trace sample rate (0.01.0)
diagnostics.otel.serviceNamestringService name tag attached to all telemetry
diagnostics.otel.flushIntervalMsintegerHow often (ms) buffered telemetry is flushed to the collector
diagnostics.otel.captureContentboolean | objectfalseCapture message content in trace spans. Pass true to capture all, or an object with granular flags per signal type
diagnostics.otel.headersobject{}HTTP headers sent with every OTLP request (e.g. Authorization)
diagnostics.otel.langfuseobjectLangfuse integration — see sub-table below
diagnostics.otel.runtimeobjectRuntime metrics collection: { enabled?: boolean (default false), intervalMs?: number, collectProc?: boolean }
diagnostics.otel.modelPricingRecord<string, Record<string, { inputPerMToken: number, outputPerMToken: number }>>Per-provider model pricing overrides for cost tracking
diagnostics.otel.langfuse sub-keys:
KeyTypeDefaultDescription
langfuse.hoststring"https://cloud.langfuse.com"Langfuse instance URL
langfuse.publicKeystringLangfuse project public key
langfuse.secretKeystringLangfuse project secret key
langfuse.filterInfraSpansbooleantrueExclude internal infrastructure spans from Langfuse traces
langfuse.sessionSpansbooleantrueGroup spans by gateway session
langfuse.sessionIdleTimeoutMsinteger1800000Idle timeout (ms) before a Langfuse session is closed (default 30 min)

Top-level diagnostics keys

KeyTypeDefaultDescription
diagnostics.captureobjectSession capture config: { mode: "off" | "manual" | "always" | "schedule", ... }
diagnostics.cacheTraceobjectCache trace config: { enabled?: boolean, filePath?: string, includeMessages?: boolean, includePrompt?: boolean, includeSystem?: boolean }
diagnostics.modelPayloadLogobjectModel payload logging config: { enabled?: boolean, usageSummaries?: boolean, rawPayloads?: boolean, captureContent?: boolean, providers?: string[], file?: string, maxQueuedBytes?: number }
diagnostics.stuckSessionWarnMsinteger60000Emit a warning if a session has not advanced for this many milliseconds
diagnostics.stuckSessionAbortMsinteger300000Abort a session that has been stuck longer than this many milliseconds
diagnostics.flagsstring[][]Debug flag strings for internal diagnostic tracing (e.g. ["rpc-trace", "session-gc"])
modelPayloadLog.enabled: true writes usage summaries to disk. To capture unredacted payloads, also set rawPayloads: true or captureContent: true. Never enable raw payload capture in production — logs will contain user messages and model responses in plaintext.

ui, cli, meta, wizard

{
  ui: { seamColor: "#FF5A2D", assistant: { name: "Wednesday", avatar: "https://..." } },
  cli: { banner: { taglineMode: "random" } },        // random | default | off
  meta: { lastTouchedVersion: "2026.3.2" },          // bookkeeping; usually agent-managed
  wizard: { lastRunMode: "local" },                  // bookkeeping
}

bindings - Multi-agent routing

{
  agents: {
    list: [
      { id: "home", default: true, workspace: "~/.openclaw/workspace-home" },
      { id: "work", workspace: "~/.openclaw/workspace-work" },
    ],
  },
  bindings: [
    { agentId: "home", match: { channel: "whatsapp", accountId: "personal" } },
    { agentId: "work", match: { channel: "whatsapp", accountId: "biz" } },
  ],
}

Config RPC (programmatic updates)

Rate-limited to 3 requests per 60 seconds per deviceId+clientIp. Pass baseHash from config.get to prevent conflicting writes. Full replace:
openclaw gateway call config.get --params '{}'  # capture payload.hash
openclaw gateway call config.apply --params '{
  "raw": "{ agents: { defaults: { workspace: \"~/.openclaw/workspace\" } } }",
  "baseHash": "<hash>"
}'
Partial update (JSON merge patch - null deletes a key, objects merge recursively, arrays replace):
openclaw gateway call config.patch --params '{
  "raw": "{ channels: { telegram: { groups: { \"*\": { requireMention: false } } } } }",
  "baseHash": "<hash>"
}'