13 KiB
Configuration Discovery and Resolution
This document describes how the coding-agent resolves configuration today: which roots are scanned, how precedence works, and how resolved config is consumed by settings, skills, hooks, tools, and extensions.
Scope
Primary implementation:
packages/coding-agent/src/config.tspackages/coding-agent/src/config/config-file.ts(re-exported fromconfig.ts)packages/coding-agent/src/config/settings.tspackages/coding-agent/src/config/settings-schema.tspackages/coding-agent/src/discovery/builtin.tspackages/coding-agent/src/discovery/helpers.ts
Key integration points:
packages/coding-agent/src/capability/index.tspackages/coding-agent/src/discovery/index.tspackages/coding-agent/src/extensibility/skills.tspackages/coding-agent/src/extensibility/hooks/loader.tspackages/coding-agent/src/extensibility/custom-tools/loader.tspackages/coding-agent/src/extensibility/extensions/loader.ts
Resolution flow (visual)
Generic helper order (`config.ts`)
┌───────────────────────────────────────┐
│ 1) ~/.omp/agent, ~/.claude, ... │
│ 2) <cwd>/.omp, <cwd>/.claude, ... │
└───────────────────────────────────────┘
│
▼
capability providers enumerate items
(native provider scans project .omp before user .omp;
other providers have their own loading rules)
│
▼
provider priority sort + capability dedup
│
▼
subsystem-specific consumption
(settings, skills, hooks, tools, extensions)
1) Config roots and source order
Canonical roots
src/config.ts defines a fixed source priority list:
.omp(native).claude.codex.gemini
User-level bases:
~/.omp/agent~/.claude~/.codex~/.gemini
Project-level bases:
<cwd>/.omp<cwd>/.claude<cwd>/.codex<cwd>/.gemini
CONFIG_DIR_NAME is .omp (packages/utils/src/dirs.ts).
Profiles
A named profile (omp --profile <name>, the --alias shortcut, or OMP_PROFILE / PI_PROFILE) relocates the OMP user base. When a profile is active, every OMP-native user-level path written here as ~/.omp/agent/... resolves to ~/.omp/profiles/<name>/agent/... instead.
The relocation is uniform across the native provider (builtin.ts) and the generic config.ts helpers, so it covers slash commands, rules, prompts, instructions, hooks, tools, extensions, settings, skills, and MCP, plus the top-level SYSTEM.md / RULES.md / AGENTS.md files and runtime state (sessions, blobs, agent.db). A profile sees only its own OMP config, never the default profile's ~/.omp/agent.
Keybindings are the one exception: a named profile merges the default profile's ~/.omp/agent/keybindings.* under its own ~/.omp/profiles/<name>/agent/keybindings.*, with the profile file overriding per binding (#4867). Keybindings describe the terminal/keyboard in front of the user, which doesn't change with the active profile, so user-level remaps keep working in every profile unless the profile explicitly overrides them. The inherited file is read-only for the profile process — legacy-format migration of the default profile's file only happens when the default profile itself runs.
The other source bases are not profile-scoped and load identically under every profile: the external-tool bases (~/.claude, ~/.codex, ~/.gemini) belong to those tools, and the project-level bases (<cwd>/.omp, <cwd>/.claude, ...) are keyed to the working directory. Throughout this document, read ~/.omp/agent as shorthand for the active profile's agent directory.
Important constraint
The generic helpers in src/config.ts do not include .pi in source discovery order.
2) Core discovery helpers (src/config.ts)
getConfigDirs(subpath, options)
Returns ordered entries:
- User-level entries first (by source priority)
- Then project-level entries (by same source priority)
Options:
user(defaulttrue)project(defaulttrue)cwd(defaultgetProjectDir())existingOnly(defaultfalse)
This API is used for directory-based config lookups (commands, hooks, tools, agents, etc.).
findConfigFile(subpath, options) / findConfigFileWithMeta(...)
Searches for the first existing file across ordered bases, returns first match (path-only or path+metadata).
findAllNearestProjectConfigDirs(subpath, cwd)
Walks parent directories upward and returns the nearest existing directory per source base (.omp, .claude, .codex, .gemini), then sorts results by source priority.
Use this when project config should be inherited from ancestor directories (monorepo/nested workspace behavior).
3) File config wrapper (ConfigFile<T> in src/config/config-file.ts, re-exported from src/config.ts)
ConfigFile<T> is the schema-validated loader for single config files.
Supported formats:
.yml/.yaml.json/.jsonc
Behavior:
- Validates parsed data against a provided Zod schema.
- Caches load result until
invalidate(). - Returns tri-state result via
tryLoad():oknot-founderror(ConfigErrorwith schema/parse context)
Legacy migration still supported:
- If target path is
.yml/.yaml, a sibling.jsonis auto-migrated once (migrateJsonToYml).
4) Settings resolution model (src/config/settings.ts)
The runtime settings model is layered:
- Global settings:
~/.omp/agent/config.yml - Project settings: discovered via settings capability (
settings.jsonandconfig.ymlfrom providers) - CLI config overlays:
omp --config <path>/ repeated--configfiles, loaded asconfig.yml-style YAML for this process only - Runtime overrides: in-memory, non-persistent
- Schema defaults: from
SETTINGS_SCHEMA
Effective precedence:
defaults <- global <- project <- CLI config overlays <- overrides
Write behavior:
settings.set(...)writes to the global layer (config.yml) and queues background save.- Project settings are read-only from capability discovery.
Migration behavior still active
On startup, if config.yml is missing:
- Migrate from
~/.omp/agent/settings.json(renamed to.bakon success) - Merge with legacy DB settings from
agent.db - Write merged result to
config.yml
Field-level migrations in #migrateRawSettings:
queueMode->steeringModeask.timeoutmilliseconds -> seconds when old value looks like ms (> 1000)- Legacy flat
theme: "..."->theme.dark/theme.lightstructure
5) Capability/discovery integration
Most non-core config loading flows through the capability registry (src/capability/index.ts + src/discovery/index.ts).
Provider ordering
Providers are sorted by numeric priority (higher first). Example priorities:
- Native OMP (
builtin.ts):100 - Claude:
80 - Codex / agents / Claude marketplace:
70 - Gemini:
60
Provider precedence (higher wins)
native (.omp) priority 100
claude priority 80
codex / agents / ... priority 70
gemini priority 60
Dedup semantics
Capabilities define a key(item):
- same key => first item wins (higher-priority/earlier-loaded item)
- no key (
undefined) => no dedup, all items retained
Relevant keys:
- skills:
name - tools:
name - hooks:
${type}:${tool}:${name} - extension modules:
name - extensions:
name - settings: no dedup (all items preserved)
6) Native .omp provider behavior (packages/coding-agent/src/discovery/builtin.ts)
Native provider (id: native) reads native config from:
- project:
<cwd>/.omp/... - user:
~/.omp/agent/...
Directory admission rules
- Slash commands, rules, prompts, instructions, hooks, tools, extensions, extension modules, and settings use a project/user root only when the root directory exists and is non-empty.
- Skills scan
<ancestor>/.omp/skillsfor each ancestor from the current working directory up to the repo root/home boundary, plus~/.omp/agent/skills, without requiring the root.ompdirectory itself to be non-empty. SYSTEM.mdandAGENTS.mdread user-level files directly and use nearest-ancestor project.omplookup for project files, but the project.ompdirectory must be non-empty. Seedocs/system-prompt-customization.mdfor the fullSYSTEM.md/APPEND_SYSTEM.mdcontract (replace vs. append, templating).
Scope-specific loading
- Skills:
<ancestor>/.omp/skills/*/SKILL.mdand~/.omp/agent/skills/*/SKILL.md - Slash commands:
commands/*.md - Rules:
rules/*.{md,mdc} - Prompts:
prompts/*.md - Instructions:
instructions/*.md - Hooks:
hooks/pre/*,hooks/post/* - Tools:
tools/*.{json,md,ts,js,sh,bash,py}andtools/<name>/index.ts - Extension modules: discovered under
extensions/(+ legacysettings.json.extensionsstring array) - Extensions:
extensions/<name>/gemini-extension.json - Settings capability:
settings.json, thenconfig.yml
Nearest-project lookup nuance
For SYSTEM.md and AGENTS.md, native provider uses nearest-ancestor project .omp directory search (walk-up) and still requires the project .omp dir to be non-empty.
7) How major subsystems consume config
Settings subsystem
Settings.init()loads globalconfig.yml+ discovered project settings capability items.- Only capability items with
level === "project"are merged into project layer.
Session title prompt override
Create TITLE_SYSTEM.md in the same config locations as SYSTEM.md / APPEND_SYSTEM.md:
# ~/.omp/agent/TITLE_SYSTEM.md
Generate a session name using lowercase `<type>:<primary-objective>`.
- Missing
TITLE_SYSTEM.mdkeeps the bundled title prompts. - Discovery uses the same project-then-user config directory pattern as
SYSTEM.md: project.omp/TITLE_SYSTEM.mdfirst, then user~/.omp/agent/TITLE_SYSTEM.mdand the other supported config bases. - The override replaces only the automatic session-title generation system prompt; normal
SYSTEM.md/APPEND_SYSTEM.mdprompt customization is unaffected. - The online path asks the title model to wrap the title in
<title>...</title>and parses it leniently from text (a plain sentence, a truncated/unclosed tag, or a stray{"title": "..."}JSON echo all still work). ATITLE_SYSTEM.mdoverride gets the wrap-in-<title>instruction appended after it. The local tiny-title path keeps the<title>...</title>prefill/stop wrapper and uses this file as its system turn.
Skills subsystem
extensibility/skills.tsloads vialoadCapability(skillCapability.id, { cwd }).- Applies source toggles and filters (
ignoredSkills,includeSkills, custom dirs). - Legacy-named toggles still exist (
skills.enablePiUser,skills.enablePiProject) but they gate the native provider (provider === "native").
Hooks subsystem
discoverAndLoadHooks()resolves hook paths from hook capability + explicit configured paths.- Then loads modules via Bun import.
Tools subsystem
discoverAndLoadCustomTools()resolves tool paths from tool capability + plugin tool paths + explicit configured paths.- Declarative
.md/.jsontool files are metadata only; executable loading expects code modules.
Extensions subsystem
discoverAndLoadExtensions()resolves extension modules from extension-module capability plus explicit paths.- Current implementation intentionally keeps only capability items with
_source.provider === "native"before loading.
8) Precedence rules to rely on
Use this mental model:
- Source directory ordering from
config.tsdetermines candidate path order. - Capability provider priority determines cross-provider precedence.
- Capability key dedup determines collision behavior (first wins for keyed capabilities).
- Subsystem-specific merge logic can further change effective precedence (especially settings).
Settings-specific caveat
Settings capability items are not deduplicated; Settings.#loadProjectSettings() deep-merges project items in returned order. Because merge applies later item values over earlier values, effective override behavior depends on provider emission order, not just capability key semantics.
9) Legacy/compatibility behaviors still present
ConfigFileJSON -> YAML migration for YAML-targeted files.- Settings migration from
settings.jsonandagent.dbtoconfig.yml. - Settings key migrations include
queueMode,ask.timeout, flattheme,task.isolation.enabled, legacytask.isolation.modevalues, removed edit modes,statusLine.plan_mode,memories.enabled, and hindsight scoping/name fields. - Legacy setting names
skills.enablePiUser/skills.enablePiProjectare still active gates for native skill source.
If these compatibility paths are removed in code, update this document immediately; several runtime behaviors still depend on them today.