Everything under ~/.carlos/.
carlos's state lives in a single directory. config.yaml holds prefs, providers, frames, and MCP servers; state.db holds the event log and FTS5 summaries; flat files hold the trust store, schedules, and per-frame artifacts.
Directory layout
~/.carlos/ ├── config.yaml # user prefs, provider keys, schedules, vault, frames ├── state.db # SQLite event log + memory FTS5 ├── trusted-workspaces.json # Phase T-2 trust store; 0600; atomic writes ├── shell-history # Phase U history file (shared across frames) ├── daemon.sock # UDS for daemon IPC (when enabled) └── frames/ └── <name>/ ├── research/<slug>-<unix-ts>.md ├── usershell/<job-id>.log ├── worktrees/<agent-id>/ └── digest/
File modes: 0700 directories, 0600 secret-bearing files (config.yaml, state.db, trusted-workspaces.json, artifact blobs), 0644 elsewhere. Atomic writes (temp + fsync + rename) for any file containing user state; see internal/config/config.go:Save.
config.yaml schema
Annotated example. This is the union of supported keys, not a minimal config.
# carlos config, sample; this is the union of the supported keys user: name: Boss providers: anthropic: api_key: sk-ant-... default_model: claude-opus-4-7 openai: api_key: sk-... default_model: gpt-5 openrouter: api_key: sk-or-... default_model: anthropic/claude-sonnet-4-6 ollama: base_url: http://localhost:11434 default_model: llama3.1:8b gemini: api_key: AIza... default_model: gemini-2.0-flash skills: convention: agents # "agents" (default, open standard) or "claude" vault: path: ~/.carlos/vault # or an Obsidian root using <root>/carlos/ daemon: enabled: false systemd_unit_installed: false frames: personal: glyph: "🧢" accent: navy mode: solo vault_subtree: personal cwd_hints: ["~/Code", "~/Documents"] work: glyph: "💼" accent: slate mode: tight provider_override: provider: openai model: gpt-5 api_key: sk-corp-... # shadows the shared pantry vault_subtree: work system_prompt_append: | You are working within the Ludus codebase ... capabilities: calendar: google mcp_servers: - name: time command: "uvx mcp-server-time" frames: ["personal"] # gated; omit to enable in every frame - name: github command: "npx -y @modelcontextprotocol/server-github" env: GITHUB_PERSONAL_ACCESS_TOKEN: ghp_... gateway: enabled: false channels: [] # populated via `carlos gateway add` schedules: [] # populated via `carlos schedule add`
Frames block
Frames are the most expressive block. See the Frames concept for what each field means. Quick summary:
| field | type | what |
|---|---|---|
glyph |
emoji | Header pill icon. |
accent |
palette name | One of rust, slate, olive, teal, plum, cream, sand, navy. |
mode |
enum | solo / tight / orchestrator. |
vault_subtree |
path | Relative to the configured vault root. |
provider_override |
object | Per-frame provider, model, key, and base URL. |
system_prompt_append |
text | Extra lines added to the system prompt. |
cwd_hints |
array | Auto-pick at fresh launch when cwd matches. |
capabilities |
object | Backend bindings (e.g. calendar: google). |
MCP server block
mcp_servers: - name: <short slug> command: "<full command line>" env: KEY: VALUE frames: ["<frame>", ...] # optional gate; omit to enable everywhere
Tools register under <server>__<tool>. /mcp lists what each server contributed at boot.
Onboarding partial re-runs
carlos onboard --only <screen> reaches any of the seven user-facing screens: name, providers, models, skills, daemon, gateway, vault. --only daemon preloads current state; --only gateway auto-skips the set-later gate when an existing config is loaded.
Legacy two-shelf layouts migrate idempotently to the frames/<name>/ layout (Phase F-17). Synthetic personal frames materialize from legacy top-level fields on first load.