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.
.envfile handling (utils/env.py, forwarded provider env vars) is a separate, out-of-scope surface — unaffected by this doc.
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 viaOPENCODE_CONFIG_DIRinconstants.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.
{
"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 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.jsonCAO discovers agent profiles by scanning multiple directories, in this order (first match wins):
- Local store —
~/.aws/cli-agent-orchestrator/agent-store/ - Provider-specific directories —
agents.dirs, keyed by provider - Extra custom directories —
agents.extra_dirs - Built-in store — bundled with the CAO package
| Key | Provider | Default Path |
|---|---|---|
kiro_cli |
Kiro CLI | ~/.kiro/agents |
claude_code |
Claude Code | ~/.aws/cli-agent-orchestrator/agent-store |
codex |
Codex | ~/.aws/cli-agent-orchestrator/agent-store |
cao_installed |
CAO 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 role → allowedTools bundles, layered on top of the built-in supervisor / reviewer / developer roles.
Skills (loaded on demand via the load_skill MCP tool) are discovered from, in order:
- Global skill store —
~/.aws/cli-agent-orchestrator/skills/ - Extra custom directories —
skills.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.
Timeouts and buffer sizes used by the CAO runtime. All values have safe defaults — only override if you experience timeouts or queue overflows.
| Setting | Default | Description |
|---|---|---|
mcp_request_timeout |
30 |
Seconds to wait for HTTP calls between the MCP server process and the CAO API. |
event_bus_max_queue_size |
1024 |
Max events buffered per subscriber queue in the internal event bus. |
provider_init_timeout |
60 |
Seconds 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_timeout |
20 |
Idle 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_max |
32768 |
Bytes 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. |
| Setting | Default | Description |
|---|---|---|
enabled |
true |
Master switch for the memory subsystem. |
compile_mode |
"llm" |
llm or append. append skips the LLM wiki-compiler entirely. |
flush_threshold |
0.85 |
Context-usage fraction that triggers a memory flush. |
compile_timeout_s |
120.0 |
Wall-clock timeout for the wiki compile call. |
learning_enabled |
false |
Opt-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_enabled |
false |
Opt-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. |
workflow_journal_capture_output |
false |
workflow_run_step projection, which retains output unconditionally either way. Retained text is size-capped (below) and cleaned through the shared audit_log sanitizer — transport hygiene (control-character stripping, size limiting), not secret redaction: a credential in a step's output is retained verbatim. |
workflow_journal_output_cap_bytes |
8192 |
Per-output byte cap applied when workflow_journal_capture_output is on; anything longer is truncated with the shared […truncated] marker. Deliberately above audit_log's 4 KiB per-field cap because a worker step's output is materially larger than a single audit field. Must be >= 1. |
workflow_journal_retention_days |
30 |
Age bound for the startup retention sweep: a run whose started_at is older than this many days is pruned (run row, steps, events, and seq high-water, in one cascade). 0 DISABLES the age bound (unlimited) — it does not mean "expire everything". Must be >= 0. |
workflow_journal_retention_count |
100 |
Run-count bound for the same sweep: runs beyond the most-recent N are pruned. Pruning is the union of the two bounds — whichever matches a run first removes it. 0 DISABLES the count bound (unlimited) — it does not mean "keep zero runs"; both bounds at 0 makes the sweep a no-op. To remove a specific run, use DELETE /workflows/runs/{id} instead. Must be >= 0. |
Turning that flag off does not stop the journal from storing step output. The
workflow_run_stepprojection persists each step's fulloutput_jsonanderrortext on every state transition, unconditionally, because the resume path and{{steps.<id>.output.<field>}}templating read them back — gating them would break both. The flag governs only the event-log output digest and the diagnostics bundle's excerpts.That retained text is served, in full, by
GET /workflows/runs/{run_id}. Its only protection is a scope requirement (cao:read/cao:write/cao:admin), and that requirement is inert unless you enable authentication: withCAO_AUTH_ENABLEDunset — the default — the dependency returns the full scope set and enforces nothing. A default local CAO server therefore serves step output and error text to anything that can reach its port.Practical consequences:
- Treat a step's output and error text as retained and readable, not as gated by a setting. A credential echoed by a step is stored verbatim and returned verbatim; the
audit_logsanitizer caps size and strips control characters, it does not detect secrets.- To limit exposure, enable auth (so the scope gate becomes real), keep the retention bounds tight, and delete runs you no longer need with
DELETE /workflows/runs/{run_id}.- An earlier revision of this table claimed the journal stored "execution metadata only … never prompt text or step output" when the flag was off. That was wrong in the security-relevant direction and is corrected above.
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.
Default-off. See ../src/cli_agent_orchestrator/ext_apps/apps.py for the ui://cao/* MCP App resource surface this gates.
| Setting | Default | Description |
|---|---|---|
enabled |
false |
Enables the MCP Apps surface (dashboard/agent/event-stream views + app tools + topology widget). |
static_dir |
null |
Override for the built apps_static/ directory (packaged/dev-tree locations are tried automatically otherwise). |
network.*keys insettings.jsonare schema-only and have no runtime effect yet.constants.pybuildsCORS_ORIGINS/ALLOWED_HOSTS/WS_ALLOWED_CLIENTSas 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_originsdepends on this reference semantics). Only theCAO_ALLOWED_HOSTS/CAO_CORS_ORIGINS/CAO_WS_ALLOWED_CLIENTS/CAO_WS_ALLOWED_ORIGINS/CAO_FORWARDED_ALLOW_IPSenv vars are read — directly inconstants.py, not throughConfigService. 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 var | Extends | Use case |
|---|---|---|
CAO_ALLOWED_HOSTS |
ALLOWED_HOSTS (Host header allowlist for TrustedHostMiddleware) |
Fronting cao-server with a reverse proxy at a non-localhost hostname. |
CAO_CORS_ORIGINS |
CORS_ORIGINS (browser origins permitted by CORS) |
Serving the web UI from a non-default port or origin. |
CAO_WS_ALLOWED_CLIENTS |
WS_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_ORIGINS |
WS_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 anyOriginwhose authority equals the requestHost(itself validated byTrustedHostMiddleware).CAO_WS_ALLOWED_ORIGINSis only for genuinely cross-origin viewers; a literal*disables the Origin check. Note thatCAO_CORS_ORIGINS="*"does not disable it — PTY access is more sensitive than ordinary CORS reads, so the escape hatch is the dedicatedCAO_WS_ALLOWED_ORIGINS="*".
auth.*keys insettings.jsonare schema-only and have no runtime effect yet.security/auth.pyis the actual authentication enforcement boundary (not a UX gate) and is deliberately kept on directos.getenvreads 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 var | Description |
|---|---|
CAO_AUTH_JWKS_URI |
Generic IdP JWKS endpoint. |
CAO_AUTH_AUDIENCE |
Expected token audience. |
CAO_AUTH_ISSUER |
Issuer advertised by the RFC 9728 PRM endpoint. |
| Setting | Default | Description |
|---|---|---|
level |
"INFO" |
Log level for the CAO server log file. |
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 var | Config path | Type |
|---|---|---|
CAO_TERMINAL_BACKEND |
terminal.backend |
str |
CAO_HERDR_SESSION |
terminal.herdr_session |
str |
CAO_MCP_APPS_ENABLED |
apps.enabled |
bool |
CAO_MCP_APPS_STATIC_DIR |
apps.static_dir |
str |
CAO_LOG_LEVEL |
logging.level |
str |
CAO_MEMORY_ENABLED |
memory.enabled |
bool |
CAO_MEMORY_COMPILE_MODE |
memory.compile_mode |
str (llm/append) |
CAO_MEMORY_FLUSH_THRESHOLD |
memory.flush_threshold |
float |
CAO_MCP_REQUEST_TIMEOUT |
server.mcp_request_timeout |
int |
CAO_EVENT_BUS_MAX_QUEUE_SIZE |
server.event_bus_max_queue_size |
int |
CAO_PROVIDER_INIT_TIMEOUT |
server.provider_init_timeout |
int |
CAO_STARTUP_PROMPT_HANDLER_TIMEOUT |
server.startup_prompt_handler_timeout |
int |
The full table lives in ConfigService.ENV_REGISTRY (services/config_service.py) — the source of truth this doc mirrors.
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 var | Config path | Type |
|---|---|---|
CAO_AUTH_JWKS_URI |
auth.jwks_uri |
str |
CAO_AUTH_AUDIENCE |
auth.audience |
str |
CAO_AUTH_ISSUER |
auth.issuer |
str |
CAO_ALLOWED_HOSTS |
network.allowed_hosts |
comma-separated list |
CAO_CORS_ORIGINS |
network.cors_origins |
comma-separated list |
CAO_WS_ALLOWED_CLIENTS |
network.ws_allowed_clients |
comma-separated list |
CAO_WS_ALLOWED_ORIGINS |
network.ws_allowed_origins |
comma-separated list |
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 var | Default | Type | Purpose |
|---|---|---|---|
CAO_HOME_DIR |
~/.aws/cli-agent-orchestrator |
str (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 var | Default | Type | Purpose |
|---|---|---|---|
CAO_PIPE_LIVENESS_CHECK_INTERVAL_S |
4.0 |
float | How often the watchdog compares live pane content against FIFO delivery, per enrolled terminal. |
CAO_PIPE_LIVENESS_TAIL_LINES |
80 |
int | Lines of live pane content compared each check (capture-pane tail size). |
CAO_PIPE_LIVENESS_STALL_CHECKS |
2 |
int | Consecutive diverging checks required before re-arming a stalled forwarder. |
CAO_PIPE_LIVENESS_MAX_REARM_FAILURES |
5 |
int | Consecutive failed re-arm attempts before the watchdog gives up on a terminal. |
CAO_PIPE_LIVENESS_COLD_START_GRACE_S |
3.0 |
float | Grace 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_ATTEMPTS |
5 |
int | Consecutive 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. |
| Method | Endpoint | Description |
|---|---|---|
GET |
/settings/agent-dirs |
Get current agent directories (merged with defaults) |
POST |
/settings/agent-dirs |
Update agent directories |
GET |
/settings/skill-dirs |
Get the global skill store path and extra skill directories |
POST |
/settings/skill-dirs |
Set extra custom skill directories |
GET |
/settings/memory |
Whether the memory subsystem is enabled |
See api.md for the full API reference.
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_backend → terminal.backend and herdr_session → terminal.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.