<!-- Compact maintainer rules retain the repository instruction line budget. -->
<!-- markdownlint-disable MD013 -->
# vtcode-config
[Root AGENTS.md](../../../AGENTS.md) | Config loading, schema, constants. `vtcode.toml` is the source of truth.
## Modules
`loader/` ConfigManager + ConfigBuilder + layers (`manager.rs` loading/security/repair/hook ownership; private `manager/persistence.rs` save methods and sparse TOML merge/pruning; see [persistence contract](../../../docs/development/configuration-reload.md#loader-and-persistence-ownership); `config.rs` config model/validation; `bootstrap.rs` feature-gated scaffold methods + private path helpers; `data/default_config.toml` embedded sample; private bootstrap tests under `loader/bootstrap/tests.rs`) | `constants/` models, env vars, URLs, tools, and shared tool limits | `core/` AgentConfig + all nested config structs (`agent/` splits `AgentConfig` from domain configs: `harness`, `approval`, `memory`, `onboarding`, `small_model`, `vibe_coding`, `prompt_suggestions`, `open_responses`, `circuit_breaker`, `codex_app_server`) | `models/` ModelId + Provider enums | `types/` ReasoningEffortLevel and related enums | `schema/` JSON Schema export (feature-gated) | `defaults/` ConfigDefaultsProvider | `auth/` auth config re-exports | `api_keys.rs` facade + `api_keys/credential_resolution.rs` source precedence | `mcp/` MCP config | `webmcp.rs` opt-in browser and remote MCP config | `acp/` ACP config | `hooks/` lifecycle hooks | `subagents/` subagent discovery (`builtin.rs` specs, `discovery.rs` loading, `parse.rs` frontmatter/TOML, `permissions.rs` tool normalization) | `network_allowlist.rs` | `core/provider_override.rs`
## Rules
- `core/anthropic_speed.rs` owns native Claude speed model/endpoint eligibility and URL precedence; keep saved preferences distinct from effective request/display speed. `ProviderModelSupport::supports_service_tier_value` owns ultrafast model eligibility; reuse it in callers and request builders. `ModelId` enum is the canonical model identifier — all model matching must go through it. `PromptCacheTtl` validates 30m/12h lifetimes; resolve explicit config > catalog > 30m. `supports_openai_explicit_prompt_cache` gates native cache policy and model-aware gap advisories; migration aliases do not establish wire support. Auxiliary explicit caching stays opt-in; see [measurement](../../../docs/development/prompt-cache-measurement.md). `constants/` is organized by domain: `models/`, `urls.rs`, `env_vars.rs`, `tools.rs`. Historical `TURN_PREVIEW_BUDGET_BYTES*` names now bound one result, not aggregate turn output; the legacy tiny-preview reserve is not enforced. `ConfigLayerStack` handles layered config (defaults → file → env → CLI) — do not bypass. `bootstrap` feature (default) scaffolds config dirs. Disable for parse-only consumers. When moving scaffold assets, preserve their output bytes and public-entrypoint tests; assert parsed TOML fields rather than searching the enclosing Rust source for template text. `schema` feature gates `vtcode_config_schema_json()` — used by `build.rs`.
## Adding a Model — **OpenRouter** (code-generated): edit `ModelId`, `Provider::OpenRouter` match, and `docs/models.json`; the build script handles the rest. **Non-OpenRouter** (manual): add the constant, `ModelId` variant and match arms, defaults if needed, preset, optional resolver update, and `docs/models.json`; gateway providers may intentionally accept explicit catalog-missing route IDs. See `adding-llm-providers` skill for the checklist
## Gotchas
- Token-efficiency defaults: `context.max_context_tokens` defaults to `0` (the resolved model/provider capacity) and bounds automatic compaction only when a positive session ceiling is configured; `tool_result_clearing.enabled` defaults to `true` (old tool results stripped past `trigger_tokens`), `tools.client_tool_search` defaults to `true` (client-local deferral for providers without hosted tool search), and `agent.system_prompt_mode`/`tool_documentation_mode` stay `minimal`/`progressive` (lean defaults; the progressive tool-schema cap is ~1,800 tokens for the builtin catalog). When changing these, update `docs/config/CONFIG_FIELD_REFERENCE.md` and the guard-rail tests. `agent.ui_surface` defaults to `inline` so interactive plan/interview HITL overlays work without configuration; `auto` and `alternate` remain explicit overrides. WebSearchProvider::backend_id() is the canonical runtime identity: `auto` and `duckduckgo` both resolve to `duckduckgo`; unsupported values use the generic allowed-variant deserialization error.
- Use `api_keys::resolve_credential_with_mode` for provider/key resolution so environment, workspace `.env`, OAuth, secure storage, and legacy migration share one precedence path. String-returning callers with a workspace use `get_api_key_in_workspace_with_mode`; `get_api_key_with_mode` has no workspace lookup. Use `provider_credential_detail_with_mode` for discovery summaries. Compatibility wrappers use the platform default storage mode.
- Custom provider profiles are exact model-ID overrides; keep `model`/`models` as the only allowlist and preserve explicit `false` capability values. Sampling fields (`temperature`, `top_p`, `top_k`, penalties, `max_tokens`, `reasoning_effort`) resolve profile > provider default; range validation lives in `CustomProviderProfileConfig::validate`. Anthropic Messages routes (native Anthropic/MiniMax and `api_format = "anthropic-messages"` profiles) drop sampling on every request: API version `2026-10-21` rejects it, so a pinned value never reaches the wire.
- Workspace/Project layers (workspace-root `vtcode.toml`, workspace `.vtcode/`) are repo-controlled: lifecycle hooks collected there land in the serde-skipped `VTCodeConfig.workspace_lifecycle_hooks` (built only by `ConfigManager::collect_workspace_lifecycle_hooks` at load) and gate the session's lifecycle engine behind per-workspace approval — never construct it outside load. UI presentation defaults and transcript-review controls belong under `ui`, and keybinding overrides stay in `ui.keybindings` so layered config remains authoritative.
- Repository-controlled Workspace/Project layers cannot define non-empty `custom_providers`, override `provider_overrides.*.base_url`/`api_key_env`, or set `auth.copilot.command`; origin checks run before provider registration. Keep executable auth and endpoint/credential overrides in system, user, or explicitly selected config files, and use the repository-safe writer for full-config persistence. Startup and reload reject repository Copilot commands; stale provider settings alone may be repaired and retried. Malformed or symlinked files fail closed, and explicit config files are never repaired.
- Keep `ui.tool_display_mode` separate from `ui.tool_output_mode`; the former controls compact-vs-expanded per-call transition summaries, while the latter controls tool output rendering. `ui.show_message_timestamps` defaults on and `ui.status_line.show_clock` defaults off: per-message footers replaced the ticking clock, which stays as an opt-in. `timeouts.long_running_command_ceiling_seconds` bounds explicit session waits; update config docs and field-reference tables with timeout changes. `webmcp` is separate from `mcp`, disabled by default, loopback-only, and explicit about origins and roots; remote MCP URLs must be HTTPS when enabled, and its proxy token field stores only an environment-variable name.
- `root::UiProgramStatusConfig` defaults on and uses normal configuration layers/schema; interactive startup and valid reloads project `ui.program_status.enabled` to the TUI. See [guide](../../../docs/development/terminal-program-status.md).
- Built-in `coordinator` is opt-in and primary-only. Its tool allowlist restricts the coordinator; retain normal inherited session permissions for scheduler-owned workers. See [matrix guide](../../../docs/development/matrix-orchestration.md).
- `accessibility.rs` hand-rolls the Windows `user32!SystemParametersInfoW` FFI: its `#[cfg(target_os = "windows")]` probe fn carries `#[expect(unsafe_code, reason = …)]` plus a `// SAFETY:` on the extern block. Non-Windows hosts never compile-check it, so keep both attached when editing that arm.
- `constants/ui.rs` owns only values with a reader outside `vtcode-ui`: shared UI copy/defaults (`DISPLAY_NAME`, header/placeholder strings, theme math, agent hues) plus `META_BULLET_SEPARATOR` as the one `" • "` join. TUI-only layout/runtime knobs (tick rates, viewport caps, modal ratios, block glyphs, ANSI palette, `HEADER_SHORTCUT_HINT`) stay in `vtcode-ui::tui::config::constants::ui` — see [DRY audit §6](../../../docs/analysis/DRY_DUPLICATION_AUDIT.md). A config copy the TUI shadows is unreachable dead code (grep cannot tell: `ui::X` resolves to the ui-local item), so adding one silently duplicates a value; `removed_shadowed_tui_constants_stay_removed` fails if a removed name returns. Crate is publishable: a public-constant removal is semver-visible, but publishing is manual (`scripts/publish_extracted_crates.sh`), so reconcile the CHANGELOG at publish time rather than restructuring ownership.