Configuration

August 1, 2026 · View on GitHub

CAO stores user configuration in a single file, ~/.aws/cli-agent-orchestrator/settings.json. This consolidates what used to be two separate files (settings.json and config.json) into one unified schema, resolved through one precedence chain:

CLI flag  >  CAO_* environment variable  >  settings.json  >  built-in default

ConfigService (services/config_service.py) is the single reader/writer behind this precedence chain. agents, skills, server, memory, terminal, and apps are fully wired: setting any of their keys in settings.json (or the mapped CAO_* env var) has a real runtime effect. network and auth are schema-only for now — see the "env-var only" callouts in those sections below and in the env-var reference.

.env file handling (utils/env.py, forwarded provider env vars) is a separate, out-of-scope surface — unaffected by this doc.

Data directory (CAO_HOME_DIR)

All CAO state lives under a single base directory, ~/.aws/cli-agent-orchestrator by default: the SQLite DB, logs, FIFOs, memory, the agent-store / agent-context profile dirs, skills, workflow scratch, and settings.json itself.

Set the CAO_HOME_DIR environment variable to relocate that entire tree:

export CAO_HOME_DIR="$HOME/.cli-agent-orchestrator"

Every derived path resolves from this value, so one override moves everything — with two exceptions noted below. CAO_HOME_DIR is read once, when CAO's constants module is first imported (the same convention as CAO_AGENTS_DIR), so export it before starting cao-server, the MCP servers, or any cao command. All CAO processes must resolve the same location. Empty or whitespace-only values are treated as unset, and tilde (~) is expanded.

When to use it. Some environments restrict or sandbox access to ~/.aws at the OS level to protect AWS credentials. Because CAO otherwise stores its data there, including the agent profiles it reads during a handoff, a locked-down ~/.aws can leave CAO unable to read its own data (a handoff then fails with Permission denied). Relocating CAO_HOME_DIR outside ~/.aws keeps CAO working while leaving those credential protections in place.

Security note. When relocating outside ~/.aws, choose a dedicated directory that is not world-readable or shared with other users. CAO creates its base directory and log/FIFO subdirectories with owner-only permissions (mode 0700), and applies a best-effort chmod to an existing base directory, but the chosen parent path should also be private since terminal logs can capture secrets and tokens.

Exceptions. Two categories of provider-specific config directories do not follow CAO_HOME_DIR:

  • ~/.aws/opencode (OpenCode provider config, managed via OPENCODE_CONFIG_DIR in constants.py) — OpenCode is told its config location at launch via env vars; a follow-up can repoint this.
  • Provider-native agent directories (~/.kiro/agents, ~/.copilot/agents) — intentionally separate since each provider manages its own agent install path independently of CAO's data tree.

settings.json schema

{
  "agents": {
    "dirs": {
      "kiro_cli": "~/.kiro/agents",
      "claude_code": "~/.aws/cli-agent-orchestrator/agent-store",
      "codex": "~/.aws/cli-agent-orchestrator/agent-store",
      "cao_installed": "~/.aws/cli-agent-orchestrator/agent-context"
    },
    "extra_dirs": [],
    "disabled_dirs": [],
    "roles": {}
  },
  "skills": {
    "extra_dirs": []
  },
  "server": {
    "mcp_request_timeout": 30,
    "event_bus_max_queue_size": 1024,
    "provider_init_timeout": 60,
    "startup_prompt_handler_timeout": 20,
    "state_buffer_max": 32768
  },
  "memory": {
    "enabled": true,
    "compile_mode": "llm",
    "flush_threshold": 0.85,
    "compile_timeout_s": 120.0
  },
  "terminal": {
    "backend": "tmux",
    "herdr_session": "cao"
  },
  "apps": {
    "enabled": false,
    "static_dir": null
  },
  "network": {
    "allowed_hosts": [],
    "cors_origins": [],
    "ws_allowed_clients": []
  },
  "auth": {
    "jwks_uri": "",
    "audience": "",
    "issuer": ""
  },
  "logging": {
    "level": "INFO"
  }
}

Every top-level key is optional — omit any section/key you don't want to override; ConfigService fills in built-in defaults.

cao config CLI

cao config get terminal.backend       # resolved value (env/file/default applied)
cao config set terminal.backend herdr # persist to settings.json
cao config list                       # every known key, resolved
cao config path                       # absolute path to settings.json

Sections

Agents (agents)

CAO discovers agent profiles by scanning multiple directories, in this order (first match wins):

  1. Local store~/.aws/cli-agent-orchestrator/agent-store/
  2. Provider-specific directoriesagents.dirs, keyed by provider
  3. Extra custom directoriesagents.extra_dirs
  4. Built-in store — bundled with the CAO package
KeyProviderDefault Path
kiro_cliKiro CLI~/.kiro/agents
claude_codeClaude Code~/.aws/cli-agent-orchestrator/agent-store
codexCodex~/.aws/cli-agent-orchestrator/agent-store
cao_installedCAO Installed~/.aws/cli-agent-orchestrator/agent-context

Override via REST API, Web UI Settings page, cao config set agents.dirs.<provider> <path>, or editing settings.json directly. Only specified providers are updated; others keep their defaults.

agents.disabled_dirs lists configured directories (defaults or extras) the user has toggled off: a disabled directory stays listed in Settings but is skipped when scanning for and loading agent profiles, so its profiles leave the active set without removing the path (GH #280/#281). Only paths that match a configured directory are accepted; entries are validated with the same path normalization the scanner uses (~, trailing slashes, and symlinks all match). Manage it via the Web UI Settings toggles, cao config set agents.disabled_dirs '[...]', or settings.json.

agents.roles defines custom roleallowedTools bundles, layered on top of the built-in supervisor / reviewer / developer roles.

Skills (skills)

Skills (loaded on demand via the load_skill MCP tool) are discovered from, in order:

  1. Global skill store~/.aws/cli-agent-orchestrator/skills/
  2. Extra custom directoriesskills.extra_dirs

skills.extra_dirs lets you keep a project's skills in the project repo (e.g. <repo>/.cao/skills) and register the directory instead of copying/symlinking each skill into the global store.

Server (server)

Timeouts and buffer sizes used by the CAO runtime. All values have safe defaults — only override if you experience timeouts or queue overflows.

SettingDefaultDescription
mcp_request_timeout30Seconds to wait for HTTP calls between the MCP server process and the CAO API.
event_bus_max_queue_size1024Max events buffered per subscriber queue in the internal event bus.
provider_init_timeout60Seconds to wait for a CLI agent to reach IDLE. Also the hard outer cap on total time a startup-prompt handler (Claude Code, Kimi, Antigravity) may run. Overridable per-profile via provider_init_timeout in the agent profile — see Agent Profile Format.
startup_prompt_handler_timeout20Idle gap, in seconds, between consecutive startup prompts (e.g. workspace trust / bypass dialogs, Kimi's upgrade dialog, Antigravity's trust/survey dialogs). The handler polls and resets this timer each time it answers a prompt; it only starts counting once the FIRST prompt has been handled, so a first dialog arriving later than this value (e.g. a cold/containerized start) is still caught — before any prompt is seen, only provider_init_timeout bounds the wait. Once at least one prompt has been handled, the handler exits after this many seconds pass with no further prompt.
state_buffer_max32768Bytes of raw terminal output StatusMonitor keeps per terminal for raw-path status detection and GET /terminals/{id}/output (mode=full). Not unbounded scrollback — a long, chatty session is truncated to this trailing window; raise it if a still-pending prompt is getting evicted before it's read back.

Memory (memory)

SettingDefaultDescription
enabledtrueMaster switch for the memory subsystem.
compile_mode"llm"llm or append. append skips the LLM wiki-compiler entirely.
flush_threshold0.85Context-usage fraction that triggers a memory flush.
compile_timeout_s120.0Wall-clock timeout for the wiki compile call.
learning_enabledfalseOpt-in switch for workflow self-learning (outcome capture via report_outcome / /outcomes). Requires enabled=true — a disabled memory subsystem forces learning off. Env override: CAO_MEMORY_LEARNING_ENABLED. See Self-Learning.
instruction_promotion_enabledfalseOpt-in switch for promoting reinforced lessons into agent profile files (cao memory promote --apply). Requires learning_enabled=true (promotion ⊂ learning ⊂ memory). Env override: CAO_MEMORY_INSTRUCTION_PROMOTION_ENABLED. ⚠️ Promoted lesson text is agent-generated: review every promote diff as an untrusted-instruction change before applying — see Self-Learning.

Terminal backend (terminal)

CAO's default backend is tmux. herdr is an experimental, opt-in alternative — a terminal-native agent runtime that exposes real-time status events instead of requiring CAO to poll and pattern-match terminal output.

{
  "terminal": {
    "backend": "herdr",
    "herdr_session": "my-session"
  }
}
  • backend: "tmux" (default) or "herdr" [EXPERIMENTAL].
  • herdr_session: the herdr session name to connect to (default "cao").

Select a backend for a single run without touching settings.json:

cao-server --terminal herdr

--terminal (CLI flag) beats CAO_TERMINAL_BACKEND (env var) beats terminal.backend (file) beats the "tmux" default — the standard precedence chain. See herdr.md for herdr-specific setup, viewing/attaching, and troubleshooting.

MCP Apps (apps)

Default-off. See ../src/cli_agent_orchestrator/ext_apps/apps.py for the ui://cao/* MCP App resource surface this gates.

SettingDefaultDescription
enabledfalseEnables the MCP Apps surface (dashboard/agent/event-stream views + app tools + topology widget).
static_dirnullOverride for the built apps_static/ directory (packaged/dev-tree locations are tried automatically otherwise).

Network (network) — env-var only

network.* keys in settings.json are schema-only and have no runtime effect yet. constants.py builds CORS_ORIGINS / ALLOWED_HOSTS / WS_ALLOWED_CLIENTS as module-level lists at import time, and Starlette's CORS/TrustedHost middleware are instantiated once at server startup holding a reference to those exact list objects (add_local_cors_origins depends on this reference semantics). Only the CAO_ALLOWED_HOSTS / CAO_CORS_ORIGINS / CAO_WS_ALLOWED_CLIENTS / CAO_WS_ALLOWED_ORIGINS / CAO_FORWARDED_ALLOW_IPS env vars are read — directly in constants.py, not through ConfigService. Routing these through the unified config would require either a live-invalidation path for the middleware's list references or restructuring how the middleware is wired; both are out of scope for this PR.

cao-server is a local-only service by default. These env vars extend (not replace) the loopback-only built-in defaults, so loopback access is preserved even when set.

Env varExtendsUse case
CAO_ALLOWED_HOSTSALLOWED_HOSTS (Host header allowlist for TrustedHostMiddleware)Fronting cao-server with a reverse proxy at a non-localhost hostname.
CAO_CORS_ORIGINSCORS_ORIGINS (browser origins permitted by CORS)Serving the web UI from a non-default port or origin.
CAO_WS_ALLOWED_CLIENTSWS_ALLOWED_CLIENTS (client IPs permitted to attach to the PTY WebSocket)Running cao-server inside Docker (host browser arrives via a bridge IP).
CAO_WS_ALLOWED_ORIGINSWS_ALLOWED_ORIGINS (extra browser Origins permitted to attach to the PTY WebSocket)Serving the terminal viewer from a page whose origin differs from the cao-server host (a separate reverse-proxy hostname or dashboard).

Security note: the WebSocket PTY endpoint is unauthenticated. Only add client IPs you actually trust to CAO_WS_ALLOWED_CLIENTS — anyone reaching the listener at one of those IPs gets full PTY access to running agent terminals.

The endpoint also enforces an Origin check to block cross-site WebSocket hijacking (CWE-1385): a browser page that is not same-origin with the cao-server host (and not in CAO_WS_ALLOWED_ORIGINS) is refused. Same-origin viewers — including the bundled UI, imported-app deployments (uvicorn cli_agent_orchestrator.api.main:app), and dynamic reverse-proxy / Codespaces hostnames — work with no configuration, because the check accepts any Origin whose authority equals the request Host (itself validated by TrustedHostMiddleware). CAO_WS_ALLOWED_ORIGINS is only for genuinely cross-origin viewers; a literal * disables the Origin check. Note that CAO_CORS_ORIGINS="*" does not disable it — PTY access is more sensitive than ordinary CORS reads, so the escape hatch is the dedicated CAO_WS_ALLOWED_ORIGINS="*".

Auth (auth) — env-var only

auth.* keys in settings.json are schema-only and have no runtime effect yet. security/auth.py is the actual authentication enforcement boundary (not a UX gate) and is deliberately kept on direct os.getenv reads in this PR, to avoid changing security-critical resolution behavior. Only the env vars below are honored.

Default-off OAuth 2.1 auth core; see security/auth.py. Auth activates only when CAO_AUTH_JWKS_URI (or AUTH0_DOMAIN) is set.

Env varDescription
CAO_AUTH_JWKS_URIGeneric IdP JWKS endpoint.
CAO_AUTH_AUDIENCEExpected token audience.
CAO_AUTH_ISSUERIssuer advertised by the RFC 9728 PRM endpoint.

Logging (logging)

SettingDefaultDescription
level"INFO"Log level for the CAO server log file.

Environment variable reference

Wired through ConfigService

Every CAO_* variable below maps 1:1 to a settings.json key and is resolved through the standard precedence chain — the env var beats the file, and both lose to an explicit CLI flag where one exists. Setting either the env var or the settings.json key has a real runtime effect.

Env varConfig pathType
CAO_TERMINAL_BACKENDterminal.backendstr
CAO_HERDR_SESSIONterminal.herdr_sessionstr
CAO_MCP_APPS_ENABLEDapps.enabledbool
CAO_MCP_APPS_STATIC_DIRapps.static_dirstr
CAO_LOG_LEVELlogging.levelstr
CAO_MEMORY_ENABLEDmemory.enabledbool
CAO_MEMORY_COMPILE_MODEmemory.compile_modestr (llm/append)
CAO_MEMORY_FLUSH_THRESHOLDmemory.flush_thresholdfloat
CAO_MCP_REQUEST_TIMEOUTserver.mcp_request_timeoutint
CAO_EVENT_BUS_MAX_QUEUE_SIZEserver.event_bus_max_queue_sizeint
CAO_PROVIDER_INIT_TIMEOUTserver.provider_init_timeoutint
CAO_STARTUP_PROMPT_HANDLER_TIMEOUTserver.startup_prompt_handler_timeoutint

The full table lives in ConfigService.ENV_REGISTRY (services/config_service.py) — the source of truth this doc mirrors.

Env-var only (settings.json value not yet honored)

These map to network.* / auth.* schema paths for documentation purposes, but only the env var is actually read — see the "env-var only" notes in the Network and Auth sections above for why.

Env varConfig pathType
CAO_AUTH_JWKS_URIauth.jwks_uristr
CAO_AUTH_AUDIENCEauth.audiencestr
CAO_AUTH_ISSUERauth.issuerstr
CAO_ALLOWED_HOSTSnetwork.allowed_hostscomma-separated list
CAO_CORS_ORIGINSnetwork.cors_originscomma-separated list
CAO_WS_ALLOWED_CLIENTSnetwork.ws_allowed_clientscomma-separated list
CAO_WS_ALLOWED_ORIGINSnetwork.ws_allowed_originscomma-separated list

Not yet routed through ConfigService

A number of other CAO_* variables (runtime/process-identity vars like CAO_TERMINAL_ID, CAO_SESSION_NAME, CAO_WORKFLOW_RUN_ID; provider-tuning vars like CAO_HERMES_*, CAO_AGENTS_DIR, CAO_API_HOST/CAO_API_PORT, CAO_PYTE_STATUS, CAO_EAGER_INBOX_DELIVERY; and CAO_AUTH_LOCAL_TOKEN) are still read ad hoc via os.getenv at their call sites, mostly in constants.py, mcp_server/server.py, security/auth.py, and the providers/* modules. These were deliberately left out of this pass to keep the diff scoped to the two surfaces issue #357 named explicitly (settings.json + config.json); folding them into the registry is a natural follow-up but not required for config unification.

Env varDefaultTypePurpose
CAO_HOME_DIR~/.aws/cli-agent-orchestratorstr (path)Base directory for all CAO state. See Data directory above.

The pipe-pane liveness watchdog (issue #388, services/fifo_reader.py) adds six more of these ad-hoc vars, read directly via _env_int/_env_float in constants.py rather than through ConfigService — they have no settings.json mapping like the rows in the table above:

Env varDefaultTypePurpose
CAO_PIPE_LIVENESS_CHECK_INTERVAL_S4.0floatHow often the watchdog compares live pane content against FIFO delivery, per enrolled terminal.
CAO_PIPE_LIVENESS_TAIL_LINES80intLines of live pane content compared each check (capture-pane tail size).
CAO_PIPE_LIVENESS_STALL_CHECKS2intConsecutive diverging checks required before re-arming a stalled forwarder.
CAO_PIPE_LIVENESS_MAX_REARM_FAILURES5intConsecutive failed re-arm attempts before the watchdog gives up on a terminal.
CAO_PIPE_LIVENESS_COLD_START_GRACE_S3.0floatGrace period after a terminal is registered before a FIFO that has never delivered a single byte is treated as a cold-start stall (harness-control#93) instead of "still booting".
CAO_PIPE_LIVENESS_MAX_COLD_START_ATTEMPTS5intConsecutive cold-start re-arm attempts (rearm() succeeded but the pipe still never delivered) before the watchdog gives up on a terminal — a separate failure class and counter from CAO_PIPE_LIVENESS_MAX_REARM_FAILURES, which only counts rearm() raising.

API Endpoints

MethodEndpointDescription
GET/settings/agent-dirsGet current agent directories (merged with defaults)
POST/settings/agent-dirsUpdate agent directories
GET/settings/skill-dirsGet the global skill store path and extra skill directories
POST/settings/skill-dirsSet extra custom skill directories
GET/settings/memoryWhether the memory subsystem is enabled

See api.md for the full API reference.

Migrating from the old two-file setup

Previously, terminal_backend / herdr_session lived in a separate ~/.aws/cli-agent-orchestrator/config.json, read inline by backends/factory.py. On first read after upgrading, if config.json exists and settings.json has no terminal section yet, ConfigService copies terminal_backendterminal.backend and herdr_sessionterminal.herdr_session into settings.json and logs the move once. config.json is left on disk untouched but is no longer read afterward — it is deprecated.