Configuration
agnostic-ai.yaml lives at the project root and is read from the current working directory. Every section is optional. The legacy filename agnostic.config.yaml still loads, with a deprecation warning.
Minimal project config
version: 1
targets: [claude, cursor]
gitignore:
enabled: true
agnostic-ai init creates one with your selected tools.
For directory-specific instructions, add scope to a rule: agnostic-ai new rule payments-context --scope services/payments. See scoped context. Set on-unsupported: error when every target must preserve scope.
Find a setting
| Change | Section |
|---|---|
| Select tools | Targets |
| Change source or output paths | Sources and Outputs |
| Keep generated files out of Git | Gitignore |
| Customize sync behavior | Sync |
| Run project behavior checks after model or CLI changes | Verify |
| Keep a hand-written file at a generated path | sync.unmanaged |
| Override settings on one machine | Local overrides |
| Understand which value wins | Precedence and Layered specs |
| Share personal instructions across projects | Global configuration |
| Inspect all fields | Top-level fields or JSON Schema |
Local overrides
agnostic-ai.local.yaml holds per-machine tweaks. It deep-merges over the base: scalars and lists replace, maps merge recursively. agnostic-ai init adds it to .gitignore.
# agnostic-ai.local.yaml (never committed)
on-unsupported: error
outputs:
claude:
dir: .claude-local # overrides base; rules-file from base survives
Editor validation
init adds this comment so YAML Language Server editors validate against docs/schemas/config.schema.json:
# yaml-language-server: $schema=https://raw.githubusercontent.com/Chemaclass/agnostic-ai/main/docs/schemas/config.schema.json
Top-level fields
| Field | Type | Default | Description |
|---|---|---|---|
version | int | 1 | Schema version, reserved for migrations. |
sources | map | .agnostic-ai/<kind>/ | Source directories. |
targets | list | 20 adapters | Adapters to emit. |
outputs | map | per target | Output path overrides. |
on-unsupported | string | warn | Unsupported kind handling. |
gitignore | map | enabled: false | Managed .gitignore block. |
sync | map | see section | Sync behavior. |
verify | map | disabled | External behavior gate. |
import | map | per source | Import behavior. |
sources
Paths are relative to the config file. Missing directories are skipped silently.
| Field | Default | Description |
|---|---|---|
agents | agents | *.md agent specs. |
skills | skills | *.md skill specs (or nested <name>/SKILL.md). |
rules | rules | *.md rule specs. |
hooks | hooks | *.yaml hook specs. |
mcps | mcps | *.yaml MCP server specs. |
outputs
outputs.<target>.* overrides where one target writes; unknown fields are ignored. Target pages list the keys and defaults, starting at the targets index. Claude Code and Codex also accept settings blocks.
outputs:
claude:
rules-dir: .claude/rules
cursor:
mcp-file: .cursor/mcp.json
targets
Default: every adapter except amp, warp, jules, goose, and augment (20 in total). Enabling those alongside codex is safe; the shared AGENTS.md body is written once. Unknown targets log a warning and are skipped. -t/--target overrides the list for one run.
agnostic-ai init and the first agnostic-ai sync can write this list through a picker. See init and the first-sync target picker.
sync
Per-target overrides live in outputs.<target>. Per-run flags such as --diff, --format, and --jobs have no config key; see sync, parallel emission, and reading a failing --check.
| Key | Default | Effect |
|---|---|---|
collision-policy | prompt | What happens when two targets write the same path. |
target-overview | false | Append a generated-locations section to each entry-point file. |
resolve-imports | passthrough | How @path lines reach targets that cannot resolve them. |
dropped-summary | false | Print a per-target summary of dropped and downgraded kinds. |
shared-skills | false | Symlink byte-identical skill folders to one copy. |
unmanaged | empty | Paths sync never touches. |
sync.collision-policy
Applies when two targets write different content to one path, such as outputs.codex.rules-file: AGENTS.md and outputs.amp.rules-file: AGENTS.md. Per-target override: outputs.<target>.collision-policy.
| Value | Behavior |
|---|---|
prompt | Default. Fail with an output collision error and a hint, which in CI suggests a non-interactive policy. |
prefer-spec | Skip the collision check. Last adapter wins. Use in CI when the overlap is intentional. |
fail | Hard error with no resolution hint. |
sync:
collision-policy: prefer-spec
sync.target-overview
When true, each entry-point file (CLAUDE.md, AGENTS.md, GEMINI.md, ...) gets an appendix listing where that tool's generated artifacts live (rules dir, agents dir, MCP file, ...), honoring outputs.<target>.* overrides.
sync:
target-overview: true
- Only the appendix differs per file. A shared entry point such as
AGENTS.mdlists each reader in its own section. - The appendix sits between
<!-- agnostic-ai:target-overview:start -->and<!-- agnostic-ai:target-overview:end -->.importstrips it, so theAGNOSTIC_AI.mdround-trip stays lossless..agnostic-ai/AGNOSTIC_AI.mdnever carries it. Every sync regenerates it, so do not hand-edit it. - Aider, whose artifacts all flow through the entry point, gets no appendix. External adapters (
agnostic-ai-adapter-<name>binaries) get no section.
sync.resolve-imports
Controls how a line holding only an @path import in AGNOSTIC_AI.md reaches targets that cannot resolve it. CLAUDE.md always keeps it, since Claude resolves imports. An @mention inside a sentence is untouched. Paths resolve from the project root.
| Value | Non-resolving targets get |
|---|---|
passthrough | Default. The @-line verbatim, as a dead reference. |
strip | Nothing: the line is dropped. |
inline | The referenced file's content between <!-- agnostic-ai:import:start <path> --> and <!-- agnostic-ai:import:end -->. import restores the @-line. A missing or unreadable file fails the sync. |
sync:
resolve-imports: inline
sync.dropped-summary
When true, sync ends by listing, per target, kinds with no surface (dropped) or emitted only behind an opt-in key or source-dir only (downgraded). It regroups capability warnings and coverage notes by target.
sync:
dropped-summary: true
dropped summary (per target):
cursor: 2 hooks dropped (unsupported)
gemini: 3 skills via outputs.gemini.emit-skills-as-commands
sync.shared-skills
When true, targets sharing the Agent Skills layout (<dir>/<name>/SKILL.md plus assets: Claude, Cursor, Codex, Amp) keep one real tree per skill; the others get relative symlinks.
sync:
shared-skills: true
- The canonical copy is
.agents/skills/<name>when emitted (Codex and Amp scan it natively), otherwise the first emitted target's tree. - Only identical rendered folders link. Overrides such as
x-cursorkeys or codex-onlyagents/openai.yamlkeep real copies for diverging targets. - Links are per skill folder, so hand-authored skills are untouched. Turning the option off, or divergence, restores real trees on the next sync. Removing a skill sweeps its tree and links.
- Without symlink support (Windows without the privilege), sync warns once and keeps real copies.
sync.unmanaged
Paths you own. sync never writes, merges, copies, or removes them.
sync:
unmanaged:
- .cursor/rules/legacy.mdc # exact path
- .claude/agents/hand-*.md # glob; `*` stays inside one path segment
- .claude/skills/legacy/ # trailing slash: everything under the directory
- Entries are project-relative with forward slashes;
\is a glob escape. A leading./or/is ignored. Globs use Gopath.Match;**is not supported. A malformed glob or an entry naming no path (.,./,/, empty) fails config load. syncprints~ skip (unmanaged) <path>(--json: underskipped, action"unmanaged").sync --check,status, anddoctornever count it as drift;doctorlists it underUser-owned, notUnmanaged config.revertanddoctor --fixnever touch it.- It stays out of the sync ledger, so removing the entry deletes nothing; the next sync rewrites the file with the provenance header.
- The
.gitignoreblock lists generated files one per line in a directory that could hold a match, instead of collapsing it. - With
sync.shared-skills, such a skill folder is never linked; an existing link becomes a real copy, keeping edits. - The list is project-wide.
agnostic-ai.local.yamlreplaces it whole. - Not covered yet: hook script bodies copied from
.agnostic-ai/scripts/into.<tool>/hooks/.
verify
verify.command is the argv list agnostic-ai verify runs. It starts the executable directly, without a shell, so pipes and redirects belong in your script.
verify:
command:
- ./scripts/verify-harness
- --strict
See the verify command for the drift check, the JSON input, and exit codes.
import
Per-source knobs for the import command. Empty blocks use per-source defaults.
import.codex.shred
Controls how agnostic-ai import codex treats AGENTS.md.
| Value | Behavior |
|---|---|
true | Default. One rule spec per ## heading. |
false | One rule spec per AGENTS.md, full body verbatim. Use when it duplicates standalone rules and you want it as a reference doc. |
import:
codex:
shred: false
on-unsupported
Applies when an adapter receives a spec kind it does not support (e.g. hooks for Cursor or mcps for Cline).
| Value | Behavior |
|---|---|
warn | Default. Log to stderr and continue. |
error | Fail the sync. |
silent | Skip without logging. |
Coverage notes
sync prints a note: line when specs of a kind exist but a target emits them only behind an inactive opt-in key, or not at all:
note: 2 skills reach gemini, opencode only via outputs.<target>.emit-skills-as-commands
note: 1 agent reaches warp only via outputs.warp.workflows-dir
Setting the named key clears the note. Notes matching the previous sync are suppressed; delete .agnostic-ai/.sync-state to show them again.
| Target | Kind | Set this to emit |
|---|---|---|
gemini | skills | outputs.gemini.emit-skills-as-commands |
opencode | skills | outputs.opencode.emit-skills-as-commands |
warp | agents | outputs.warp.workflows-dir |
aider | agents, skills | outputs.aider.rules-file |
zed | hooks | outputs.zed.tasks-file |
kilo | agents with tools | None. Use x-kilo: {permission: {...}}. |
gemini | agents with tools beyond Read/Write/Edit/Bash/Grep/Glob/WebFetch/WebSearch | None. Use x-gemini: {tools: [...]} or drop tools. |
crush | hooks not on PreToolUse | None. Crush runs PreToolUse only. |
gitignore
| Field | Default | Description |
|---|---|---|
enabled | false when absent; agnostic-ai init writes true | Every sync rewrites a managed .gitignore block listing every path the configured adapters emit. |
path | .gitignore | Another file, for monorepos or local-only ignore files. |
allow | empty | Gitignore globs written verbatim as ! lines at the end of the block, so a tracked file (e.g. a testdata/AGENTS.md fixture) is not ignored. |
An existing config with no gitignore key stays false. sync --gitignore and init --gitignore override it per run; see the CLI reference.
The block sits between # >>> agnostic-ai (managed) >>> and # <<< agnostic-ai (managed) <<<. Lines outside it are kept, and an unchanged sync keeps the file mtime. Its header says to edit specs, and that a fresh clone or git worktree lacks these paths until sync runs (see post-checkout hook).
- Entries are root-anchored (
/AGENTS.md, notAGENTS.md), so nested same-named files are not ignored. - Files collapse to their generated subdirectory (
/.claude/rules/), never higher, so siblings such as.claude/settings.jsonor.claude/hooks/stay visible. - The block always holds
agnostic-ai.local.yaml,/.agnostic-ai/.sync-state, and/.agnostic-ai/packs/, seeded byiniteven withgitignore.enabled: false.init,sync, orpacks addmoves old loose copies into the block. - A target can add entries of its own, such as Claude Code's local settings and agent memory.
Watched inputs
sync --watch re-emits when the config files, any sources directory, .agnostic-ai.local/, or .agnostic-ai/overlays/ change. Overlays hold keys the spec layer does not own, such as Claude statusLine or Codex [profiles.*]. See sync --watch.
Path semantics
sourcesandoutputspaths are relative to the directory holdingagnostic-ai.yaml.- Output directories are created on demand. Existing files are overwritten.
Entry-point files
sync writes .agnostic-ai/AGNOSTIC_AI.md plus one root entry-point file per enabled target, all sharing the canonical pointer body. See the per-target table.
Setting outputs.<target>.rules-file: <path> restores the legacy layout: the adapter writes one merged document at <path> and sync skips the pointer body for that target. Two adapters writing different content to one path fail unless you set sync.collision-policy: prefer-spec.
Per-target paragraphs
.agnostic-ai/AGNOSTIC_AI.md accepts the ::target / ::targets / ::end fences from spec bodies.
Shared conventions for every tool.
::target gemini
Gemini reads `GEMINI.md` only. Load rules from `.gemini/rules/`.
::end
::targets codex amp
Run `make preflight` before you stop.
::end
- Unfenced content goes to every entry-point file.
- A fenced block reaches a file when any of its readers is listed, so
::target codexreaches everyAGENTS.mdreader. A shared file is never split. - Markers never reach output and must start at column 0; indent a sample that shows one.
agnostic-ai validateflags a fence naming an unknown target, or a built-in target that reads no entry-point file.agnostic-ai import <tool>keeps a fenced source when the imported file equals what sync renders (under the defaultsync.resolve-imports: passthrough); otherwise it overwrites the source and warns.
Precedence
Last wins:
- Built-in defaults
agnostic-ai.yamlagnostic-ai.local.yaml- CLI flags (e.g.
agnostic-ai sync -t claude)
Layered specs
Specs load from three layers, lowest first. Higher layers override by spec name per kind; new names append. agnostic-ai list shows each spec's layer.
| Layer | Root | Loaded when |
|---|---|---|
| packs | .agnostic-ai/packs/ from agnostic.packs.lock | packs are installed |
project | agnostic-ai.yaml sources paths | always |
project-user | <project>/.agnostic-ai.local | directory exists |
Only project honors custom sources paths; project-user uses fixed kind directories. Add .agnostic-ai.local/ to .gitignore. $AGNOSTIC_AI_HOME is not a project layer.
Global configuration
agnostic-ai sync --global installs user-level instructions, rules, hooks, and skills for 22 of the 25 targets (global output lists paths). It works from any directory and loads no agnostic-ai.yaml, packs, local overrides, or project specs.
Source root: $AGNOSTIC_AI_HOME, or ~/.agnostic-ai/ when AGNOSTIC_AI_HOME is unset.
~/.agnostic-ai/
├── AGNOSTIC_AI.md
├── rules/*.md
├── hooks/*.yaml
└── skills/<name>/SKILL.md
- It targets every supported tool by default. Which
syncflags it accepts is in the CLI reference. - Nested rules and rules with scope, path, glob, or target conditions are rejected. Agents, commands, MCP servers, settings, inheritance, and merging with project specs are unsupported.
- Output is real files, never symlinks. Ownership is recorded per target in
$AGNOSTIC_AI_HOME/state/global.json. Sync keeps unrelated text, JSON keys, hooks, and skills, and removes only recorded artifacts for the targets in the run, so--onlynever sweeps another target. - An unmanaged skill or rule collision, damaged marker, invalid native JSON, or corrupt state stops the run before writes.
- Empty surfaces create nothing: no instructions file (a recorded one is removed) and no hooks file.
- Native tool precedence applies when global and project configuration both exist.
Ordinary agnostic-ai sync does not load ~/.agnostic-ai/. Move project-only defaults, and any agents, MCP servers, commands, settings, reviews, environments, or ignore specs, into a project's .agnostic-ai/ or a pack. A repository's .agnostic-ai/ stays project-specific despite the shared basename.