Cascading Configuration Resolution

OpenClaw uses a 5-layer cascading configuration system for flexible runtime configuration.

Key Files

  • src/config/runtime-overrides.ts (92 lines): In-memory runtime overrides
  • src/config/io.ts: File-based config loading and merging
  • src/config/env-vars.ts: Environment variable application

Resolution Layers (Highest to Lowest Priority)

Layer 1: Runtime Overrides (In-Memory)

  • Highest priority
  • Set via setConfigOverride(path, value)
  • Persisted in memory only, not written to config file
  • Used for temporary overrides during execution

From src/config/runtime-overrides.ts:

let overrides: OverrideTree = {};

export function setConfigOverride(pathRaw: string, value: unknown): {
  ok: boolean;
  error?: string;
} {
  const parsed = parseConfigPath(pathRaw);
  if (!parsed.ok || !parsed.path) {
    return { ok: false, error: parsed.error ?? "Invalid path." };
  }
  setConfigValueAtPath(overrides, parsed.path, sanitizeOverrideValue(value));
  return { ok: true };
}

Layer 2: Environment Variables

  • Applied from process.env with OPENCLAW_* prefix
  • Config can define additional env vars via config.env
  • Environment variables are resolved during config load

Layer 3: Agent-Specific Config

  • Configuration scoped to a specific agent ID
  • Defined in config.agents[agentId]
  • Merged with higher and lower layers

Layer 4: Session-Level Config

  • Configuration specific to a session
  • Stored in session metadata
  • Overrides global defaults but yields to higher layers

Layer 5: Global Defaults

  • Lowest priority, fallback values
  • Defined in src/agents/defaults.ts, src/config/defaults.ts
  • Applied when no higher layer provides a value

Merge Strategy

Deep Merge for Objects

function mergeOverrides(base: unknown, override: unknown): unknown {
  if (!isPlainObject(base) || !isPlainObject(override)) {
    return override; // Non-objects: replace entirely
  }
  const next: OverrideTree = { ...base };
  for (const [key, value] of Object.entries(override)) {
    if (value === undefined || isBlockedObjectKey(key)) {
      continue; // Skip undefined and prototype-polluting keys
    }
    next[key] = mergeOverrides((base as OverrideTree)[key], value);
  }
  return next;
}

Arrays: Replace Entirely

  • Arrays are not merged element-by-element
  • Higher-priority array completely replaces lower-priority array

Prototype-Polluting Keys Blocked

From src/config/prototype-keys.ts:

const BLOCKED_KEYS = new Set([
  "__proto__",
  "constructor",
  "prototype",
]);

export function isBlockedObjectKey(key: string): boolean {
  return BLOCKED_KEYS.has(key);
}

Tool Policy Cascading

Tool policies use a similar 7-layer cascading allowlist system:

  1. Runtime overrides
  2. Environment variables
  3. Agent-specific policy
  4. Session-level policy
  5. User-level policy
  6. Tool-specific defaults
  7. Global defaults

Each layer is an allowlist that merges with lower layers.

Config Hot-Reload

OpenClaw supports hot-reloading configuration files via chokidar:

  • Watcher: Monitors config file for changes
  • Debounce: 300ms delay to batch rapid changes
  • Modes:
    • off: No watching
    • restart: Restart process on change
    • hot: Reload config in-place
    • hybrid: Hot-reload with restart fallback

Cross-References