Skip to content

Entrada de configuración openclaw.json

源码版本v2026.6.11

Responsabilidad

openclaw.json es el único archivo de configuración (config) con autoría humana en OpenClaw: puerto del gateway, provider de modelo, credenciales de canal, rutas de carga de skills, cron, política de memoria, approvals, hooks — todo se escribe en este único archivo JSON5. En runtime toda la configuración se lee de aquí, y tras expansión de include, sustitución de variables de entorno, validación por Zod schema e inyección de defaults, se convierte en el objeto OpenClawConfig en memoria.

No es solo un archivo «leer una vez y listo»: la CLI config set/patch/unset escribe de forma atómica, el snapshot runtime se compara con el contenido en disco para evitar condiciones de carrera, y ante problemas se puede restaurar desde el backup last-known-good. Por tanto esta entrada asume cinco funciones: leer, escribir, validar, respaldar y migrar.

Motivación de diseño

¿Por qué un único archivo en lugar de un directorio disperso? En los primeros días de OpenClaw, provider/channel/memory vivían en JSON separados, y el resultado era que cambiar un provider requería tocar tres archivos, y al actualizar un plugin de canal tocaba esparcir lógica de migración por todas partes. Al converger en un único openclaw.json, version stamp, backup y log de auditoría cuelgan todos del mismo archivo, y openclaw doctor solo escanea una ruta para hacer un check-up.

Pero un único archivo también tiende a inflarse, así que se soportan dos vías de escape:

  • La directiva $include separa grandes bloques de config de provider/channel a subarchivos, dejando solo referencias en la raíz.
  • La sustitución de variables ${VAR} permite sacar los secrets del archivo y llevarlos a un gestor.

Estas dos características dejan que openclaw.json pueda entrar en un repo git (tras ofuscar secrets) y a la vez correr en despliegues tipo Docker/Tailscale que solo reciben variables de entorno.

Archivos clave

Flujo de datos

Al arrancar, createConfigIO primero fija las constantes de ruta y luego pasa por loadConfigLocal (loadConfigLocal:1654). Pasos clave de la ruta de lectura:

typescript
function loadConfigLocal(options: { skipSuspiciousRecovery?: boolean } = {}): OpenClawConfig {
  try {
    maybeLoadDotEnvForConfig(deps.env);
    const envBeforeRead = snapshotEnv(deps.env);
    if (!deps.fs.existsSync(configPath)) {
      // ... carga shell env fallback y devuelve config vacía
      return {};
    }
    const raw = deps.fs.readFileSync(configPath, "utf-8");
    const parsed = deps.json5.parse(raw);
    const readResolution = resolveConfigForRead(
      resolveConfigIncludesForRead(parsed, configPath, deps),
      deps.env,
      deps.lowerPrecedenceEnv,
    );
    // ... shipped plugin install records migración, validate, materialize

Tres cosas a notar: maybeLoadDotEnvForConfig solo carga dotenv de verdad cuando env es process.env (un env inyectado en tests no se toca); envBeforeRead se usa al escribir de vuelta para detectar «si env cambió durante la lectura»; resolveConfigIncludesForRead lee los archivos destino de $include y luego se los pasa a resolveConfigForRead para sustitución de ${VAR}. El orden es clave: include antes que sustitución de env, así los campos traídos por include también pueden usar ${VAR}.

La ruta de escritura pasa por writeConfigFile (writeConfigFile:2826). Cuando hay un snapshot runtime, primero hace diff de la config runtime que da el caller y la proyecta de vuelta al source:

typescript
export async function writeConfigFile(
  cfg: OpenClawConfig,
  options: ConfigWriteOptions = {},
): Promise<ConfigWriteResult> {
  options.assertConfigPathForWrite?.();
  const io = createConfigIO({ /* ... */ });
  assertConfigWriteAllowedInCurrentMode({ configPath: io.configPath });
  let nextCfg = cfg;
  const runtimeConfigSnapshot = getRuntimeConfigSnapshotState();
  const runtimeConfigSourceSnapshot = getRuntimeConfigSourceSnapshotState();
  const hadBothSnapshots = Boolean(runtimeConfigSnapshot && runtimeConfigSourceSnapshot);
  if (hadBothSnapshots) {
    const runtimePatch = createMergePatch(runtimeConfigSnapshot!, cfg);
    nextCfg = coerceConfig(applyMergePatch(runtimeConfigSourceSnapshot!, runtimePatch));
  }

Este paso garantiza que lo que el caller escribe de vuelta es «la authoring-state del usuario», no la forma contaminada por defaults runtime — si no, los defaults del schema se solidificarían en el archivo y no se podría revertir al cambiar de versión.

El modo patch CLI (runConfigPatch:2279) recibe un objeto JSON5 de patch y lo mergea recursivamente en la config existente:

typescript
function mergeConfigValue(existing: unknown, patch: unknown, path: PathSegment[]): unknown {
  if (isProviderModelListPath(path) && Array.isArray(existing) && Array.isArray(patch)) {
    return mergeModelArrays(existing, patch);
  }
  if (isPlainRecord(existing) && isPlainRecord(patch)) {
    // Deep merge: objetos recursivo, no-objetos sobreescribe

Arrays como models[] van por mergeModelArrays, que deduplica por campo id y luego mergea elementos, en lugar de reemplazar — añadir un model a un provider no borra todos los models existentes.

Relación entre las tres rutas de lectura, escritura y patch:

Límites y fallos

  • Path drift: al arrancar, el gateway debe llamar primero pinRuntimePaths (pinRuntimePaths:236-245), si no, OPENCLAW_CONFIG_PATH setteado después del import de módulos leería valores viejos, causando el split «leo de una ruta, escribo en otra».
  • Fallback legacy: resolveConfigPath escanea primero clawdbot.json y .clawdbot/, si no existen va al nuevo path. Al actualizar un despliegue viejo con ambos directorios, gana el nuevo; pero si el usuario deja el archivo viejo en ~/.openclaw/ por error, el proceso nuevo también lo lee — el comando doctor escanea específicamente este split.
  • $include path traversal: por defecto solo puede referenciar archivos del mismo directorio o subdirectorios de openclaw.json. Para cruzar directorios hay que settear explícitamente $OPENCLAW_INCLUDE_ROOTS, y cada root se expande con tilde + se deduplica + se rechazan paths no absolutos (include roots filter:128-141).
  • ${VAR} ausente: no lanza error, solo warn «feature using this value will be unavailable» (src/config/io.ts:1950-1956), el gateway entra modo degradado en lugar de rechazar el arranque. Pero si falta la apiKey del provider, fallará al hacer la primera petición.
  • Modo Nix read-only: con OPENCLAW_NIX_MODE=1, assertConfigWriteAllowedInCurrentMode rechaza toda escritura, la config se gestiona externamente vía Nix (resolveIsNixMode:16-18).
  • Escritura atómica: replaceFileAtomic escribe a un archivo temporal y luego rename, permisos de directorio 0o700 y de archivo 0o600, en Windows usa un fallback por copia.
  • Bloqueo de caída brusca de tamaño: antes de escribir compara previousBytes vs nextBytes, si cae por debajo del 50% sin flag allowConfigSizeDrop, rechaza la escritura — evita que un patch borre de golpe todo un bloque de provider.
  • last-known-good: cada lectura exitosa actualiza la huella lastKnownGood (hash + bytes + mtime + dev/ino); si la siguiente lectura detecta caída de size o el campo gateway mode borrado, restaura automáticamente desde .bak y deja una línea de log de auditoría con evento config.observe.
  • Aviso de future version: cuando meta.lastTouchedVersion del archivo es más nuevo que el binario actual, warn al usuario sugiriendo que PATH podría apuntar a una versión vieja de openclaw — evita «una config escrita por versión nueva se corrompe al leerla con versión vieja».

Resumen

openclaw.json es una entrada única, pero detrás hay un ciclo de vida completo de lectura-validación-materialización-escritura-auditoría. La ruta de lectura expande include y env capa por capa, la ruta de escritura proyecta la forma runtime de vuelta al source, y el modo patch ofrece al CLI edición incremental estructurada. Cómo el schema define la forma legal de cada campo se cubre en Sistema de schemas Zod; los campos concretos del provider (baseUrl/api/auth/apiKey/models[]) en Providers; la config del lado canal en Configuración de canal; cómo el proceso residente usa la config leída en Daemon.

Referencias oficiales: Documentación de Configuration · README.