Configuration
agnostic-ai.yaml lives at the project root. Commands read it from the current working directory. Every section is optional. The legacy filename agnostic.config.yaml still loads, with a deprecation warning. Run agnostic-ai migrate to rename it.
Minimal project config
version: 1
targets: [claude, cursor]
gitignore:
enabled: true
agnostic-ai init creates one with your selected tools.
For directory-specific instructions, give a rule a scope. See scoped context. Set on-unsupported: error when every target must keep the scope.
Find a setting
| Change | Section |
|---|---|
| Select tools | Targets |
| Share session state across tools | Built-ins |
| Change paths | Sources, Outputs |
| Keep generated files out of Git | Gitignore |
| Tune sync | Sync, sync.unmanaged |
| Gate on project checks | Verify |
| Pin the agnostic-ai release | requires |
| Instruction size warnings | lint |
| Silence a known coverage note, or fail on the rest | coverage |
| Per-machine or personal overrides | Local overrides, Local spec layers |
| Which value wins | Precedence, Layered specs |
| Cross-project instructions | Global configuration |
| All fields | Top-level fields, JSON Schema |
Local overrides
agnostic-ai.local.yaml holds per-machine changes. It deep-merges over the base: scalars and lists replace, and maps merge recursively. agnostic-ai init adds it to .gitignore. Personal specs and instructions go in .agnostic-ai/local/.
# 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 editors validate the file against the schema of the release that wrote it.
# yaml-language-server: $schema=https://raw.githubusercontent.com/Chemaclass/agnostic-ai/v0.72.0/docs/schemas/config.schema.json
Top-level fields
A key not listed in this reference fails every command that reads the config with AAI-004. The error names the file, line, and closest known key. This also applies to agnostic-ai.local.yaml, so a typo can't drop a setting without you noticing.
| Field | Type | Default | Description |
|---|---|---|---|
version | int | 1 | Schema version, reserved for migrations. |
requires | string | none | agnostic-ai releases the specs work with: a minimum, one release, or a range. |
sources | map | .agnostic-ai/<kind>/ | Source directories. |
targets | list | 20 adapters | Adapters to emit. |
builtins | list | none | Built-in specs to enable. init enables handoff. |
outputs | map | per target | Output path overrides. |
models | map | none | Model tiers that agents, skills, commands, and settings name. |
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. |
lint | map | see section | Budgets for always-loaded text. |
doctor | map | see section | Opt-in diagnostic checks. |
coverage | map | none | Accepted coverage notes and the note gate. |
Built-ins
builtins selects specs bundled inside the binary. Existing projects enable none by default. agnostic-ai init writes builtins: [handoff] for new projects.
requires: ">=0.80.0"
builtins: [handoff]
The available names are handoff (the skill) and handoff-hook (optional Git snapshots and resume notices). See session handoffs for supported targets, runtime requirements, and Factory's display setting. Unknown names fail as AAI-004 and list the valid names. A project, pack, or personal spec with the same kind and name takes precedence.
Enable both explicitly to add hooks:
builtins: [handoff, handoff-hook]
handoff-hook emits for Claude Code, Codex, Gemini CLI, Qoder CLI, and Factory / Droid. Factory's notice requires user-owned showHookOutput: true; its hooks are project-only.
The same list works in the global home config. A builtins list in either local config replaces the shared list. Set builtins: [] to disable it there. Removing a built-in removes its ledger-owned outputs on the next sync.
Bump requires to at least 0.80.0 when enabling built-ins so an older binary cannot silently omit them. Editors using an older schema flag the key until the release ships. An upgrade that changes built-in text causes sync --check drift until you sync.
Legacy merged documents include built-in skill instructions inline.
requires
Names the agnostic-ai releases your specs work with. Pick the form that matches what you need to protect:
requires: ">=0.71.0" # minimum: 0.71.0 or any newer release
requires: "0.73.0" # exact: only 0.73.0; "=0.73.0" means the same
requires: ">=0.73.0 <0.74.0" # range: 0.73.0 and its patch releases
- Minimum: the specs use a feature from that release, and generated files stay out of Git. Newer releases keep working.
- Exact: you commit generated files. Another release can write different bytes, so
sync --checkwould fail in CI. When npm installs the tool, pin the same release inpackage.json. - Range: accept patch releases but not the next minor one.
A binary outside the value stops every command that reads your specs. It stops before reading specs or writing files. The error is AAI-005. Its main fix depends on the installed release:
- Newer than the project permits:
agnostic-ai upgrade --requiresadopts the installed release. It sets an exact pin and matching schema URL, then syncs. Run it after your package-manager upgrade, with that manager's CLI, such aspnpm exec agnostic-ai upgrade --requires. - Older than the project needs: install a fitting release with
agnostic-ai upgrade,upgrade --version vX.Y.Z, or the project's package manager. The message names the command.
To keep an intentional older pin, install the release it names. upgrade --requires updates the base config and any existing local override. It handles project config only. For a global home pin, edit the file the error names.
sync --watch stops when a pulled config puts the binary outside requires.
A value is one or more terms separated by spaces. Every term must hold: >=X.Y.Z, <X.Y.Z, <=X.Y.Z, =X.Y.Z, or a bare X.Y.Z. Anything else fails as AAI-004, naming the file. A build from source (go run, or a commit after a tag) is not a release, so it warns once instead.
Releases before 0.70.0 ignore the key. Releases before 0.74.0 read only Build the candidate with the release it stands for: That build is checked as release X.Y.Z. Setting agnostic-ai.local.yaml can replace the value. An empty requires: there turns the check off. The global home config accepts the key too.Older releases
>=X.Y.Z. They stop on an exact or range value with AAI-004, whose text suggests >=X.Y.Z. Install the release the value names instead of editing requires.Test a release candidate against a pinned project
go build -ldflags "-X github.com/chemaclass/agnostic-ai/internal/cli.candidateVersion=X.Y.Z" -o agnostic-ai ./cmd/agnostic-ai
main.version changes only what --version prints.
sources
Missing directories are skipped without a message. See path semantics.
Source roots can be directory symlinks on macOS or Linux, or directory junctions on Windows. Source provenance uses the configured path.
Directory links nested below a source root are not traversed. A cycle in a source-root link fails loading with a path error. A link with a missing target is skipped.
| 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. An unknown field fails with AAI-004. 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
outputs.<target>.agents: skill writes agents as on-demand skills on Amp, Crush, Warp, and Zed, which have no subagents. See agents as skills.
outputs.kiro.commands-dir overrides .kiro/prompts/ for project command emission and import. See Kiro commands.
Two Codex keys change how rules and permissions are written:
outputs.codex.nested-glob-rulesdefaults totrue: exact whole-subtree rule selectors write nestedAGENTS.mdfiles. Set it tofalseto inline those rules in the root file. Filename filters and root-file selectors stay inline, with an always-loaded note underon-unsupported. See Codex rules.outputs.codex.exec-policies-from-permissionsdefaults tofalse. Set it totrueto translate simple Bash entries from portable Settings specs andoutputs.claude.settings.permissionsinto Codex command rules. Explicit inline, file, or imported Codex policies take precedence. Every translated rule matches a command prefix, so extra arguments match too. Sync notes each exactallowrule that Codex widens.on-unsupported: errorfails on this widening. See Bash permission translation for limits and LINT021 drift checks.
models
Name each model role once, then write the role in specs instead of a vendor's model id:
models:
frontier: {claude: fable, codex: astra, effort: xhigh}
strong: {claude: opus, codex: sol, effort: high}
balanced: {claude: sonnet, codex: terra, effort: medium}
fast: {claude: haiku, codex: luna, effort: low}
An agent with model: fast gets haiku in Claude Code and gpt-6-luna in Codex, both at low effort. Other targets keep their default model. Skills, commands, and settings specs name tiers the same way. Change a tier here and every spec that names it follows.
Claude Code resolves opus, sonnet, haiku, and fable to its latest models itself. Codex documents only exact ids, so agnostic-ai resolves these aliases when it writes Codex files:
| Codex alias | Model id |
|---|---|
sol | gpt-6.1-sol |
luna | gpt-6-luna |
astra | gpt-6-astra |
terra | gpt-5.6-terra |
The terra alias was added after v0.77.0. On v0.77.0, use its full ID, gpt-5.6-terra.
Each release fixes the ids. A project that pins requires writes the same ids on every machine. Upgrading agnostic-ai changes them. The first sync after such an upgrade prints note: codex: sol now resolves to <new id> (was <old id>) once. explain <spec> shows the resolution, such as sol → gpt-6.1-sol.
An alias works anywhere Codex reads a model: a tier, model.codex, a shared model, or a settings spec. x-codex.model is written as given, so it can send an id the table does not know.
| Tier key | Value |
|---|---|
| A target name | That target's model id. |
default | The model id for each target the tier does not name. |
effort | One effort for every target, or a per-target map such as {claude: xhigh, codex: high}. |
- A
modelthat names no tier stays a literal model id. - A tier with only
effortkeeps each tool's default model. - A tier with neither a model nor
effortfails to load, and so does any other key (AAI-004, naming the closest target). - Per-spec overrides and precedence: model tiers.
explain <spec>shows the model and effort each target gets. lintwarns when:- a tier a spec names has no entry and no
defaultfor a target that writes the spec, - a tier is named like a Claude model (LINT025),
- a tier's
defaultis a Claude model another target cannot load (LINT026).
- a tier a spec names has no entry and no
- A tier in
agnostic-ai.local.yamlreplaces the same-name tier whole. Leaving a target out drops its shared model. - The global home config accepts the key too.
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. -t/--target overrides the list for one run.
A likely typo of a built-in target, such as cursr, fails before any file is written. Any other unknown name logs a warning and is skipped. It may be an external adapter missing from this machine's PATH.
init and the first sync can write this list through a picker.
sync
Per-run flags such as --diff, --format, and --jobs have no config key. See sync 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. |
output-manifest | false | Write .agnostic-ai/outputs.lock, the committed list of generated paths. |
allow-global-names | empty | Names shared with the global home that sync does not warn about. |
global-name-clash | warn | Whether sync warns about names shared with the global home. |
sync.collision-policy
This 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. In CI, it acts as 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 hint. |
sync:
collision-policy: prefer-spec
sync.target-overview
When true, each entry-point file (CLAUDE.md, AGENTS.md, GEMINI.md, ...) gets an appendix. It lists where that tool's generated files live. It honors outputs.<target>.* overrides.
sync:
target-overview: true
- A shared entry point such as
AGENTS.mdlists each reader in its own section. - Every sync regenerates the appendix, between
<!-- agnostic-ai:target-overview:start -->and<!-- agnostic-ai:target-overview:end -->. Do not edit it by hand.importstrips it. - Aider and external adapters get no appendix.
sync.resolve-imports
Sets how a line holding only an @path import in AGNOSTIC_AI.md reaches targets that can't resolve it. CLAUDE.md always keeps it. An @mention inside a sentence is untouched, and so is a line inside a fenced code block. Paths resolve from the project root.
| Value | Non-resolving targets get |
|---|---|
passthrough | Default. The @-line as written, 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 with a per-target list of kinds that were dropped or downgraded. Dropped means the target has no surface for the kind. Downgraded means it is emitted only behind an opt-in key, or only in the source dir. The list regroups capability warnings and coverage notes by target.
sync:
dropped-summary: true
sync.shared-skills
When true, targets that share the Agent Skills layout keep one real tree per skill. The others get relative symlinks. That layout is <dir>/<name>/SKILL.md plus assets, used by Claude, Cursor, Codex, and Amp.
sync:
shared-skills: true
- The canonical copy is
.agents/skills/<name>when emitted, otherwise the first emitted target's tree. - Only identical rendered folders link. Per-target overrides such as
x-cursorkeep real copies. - Turning the option off, or a folder that diverges, restores real trees on the next sync.
- 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. Globs use Go
path.Match;**is not supported. - A malformed glob or an empty entry fails config load.
syncprints~ skip (unmanaged) <path>.sync --check,status, anddoctornever count it as drift.revertanddoctor --fixnever touch it.- Removing the entry deletes nothing. The next sync rewrites the file.
sync.shared-skillsnever links such a skill folder.- The list is project-wide.
agnostic-ai.local.yamlreplaces it whole. - Hook script bodies copied from
.agnostic-ai/scripts/are not covered.
sync.output-manifest
Use this when a repository commits its generated files. A full sync writes .agnostic-ai/outputs.lock: one line per generated path, with the content sum sync wrote. sync --check fails when the file is missing or out of date. Commit it with the outputs.
sync:
output-manifest: true
The manifest is CI's record of what sync wrote. .sync-state is never committed, and a JSON output carries no header to prove its origin.
- With no
.sync-state,doctorandsync --checkcount a tracked file the manifest lists but no spec produces as a leftover.doctor --fixremoves it. - A file edited since sync wrote it no longer matches its sum, so it is left for you.
sync --check --againstreads the manifest of the state it checks. It catches a deleted spec's outputs even in a one-commit shallow clone, as long as the manifest was not regenerated in the same commit.
sync.allow-global-names
Skill and agent names this project shares with the global home on purpose. sync prints no shared-name warning for a listed name. Other shared names still warn.
# agnostic-ai.local.yaml
sync:
allow-global-names: [gh-issue, gh-issues]
- The home exists only on your machine, so
agnostic-ai.local.yamlis the usual place for the list. - Names match the way Claude Code folds skill names: case, spacing, and invisible characters do not count.
doctorstill lists each allowed name under Global names, marked as allowed.agnostic-ai.local.yamlreplaces the list whole.
sync.global-name-clash
What sync does with every name this project shares with the global home: warn prints the warning, ignore prints nothing.
# agnostic-ai.local.yaml
sync:
global-name-clash: ignore
- Use it when a project shadows global skills or agents as a rule. To accept a few names and keep warning about the rest, use
sync.allow-global-names. - With
warn, names insync.allow-global-namesstill print nothing. doctorstill lists each shared name under Global names, marked as ignored.- Any other value fails config load.
verify
verify.command is the argv list agnostic-ai verify runs. It starts the executable directly, without a shell. Put pipes and redirects 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 options for the import command. Empty blocks use per-source defaults.
import.codex.shred
Sets how agnostic-ai import codex treats a nested AGENTS.md. The root file always lands in .agnostic-ai/AGNOSTIC_AI.md.
| Value | Behavior |
|---|---|
| unset | Default. A hand-written file becomes one rule with its full body, so sync writes it back as it was. A file sync wrote splits back into the rules it came from. |
true | One rule spec per ## heading, also for a hand-written file. |
false | One rule spec per AGENTS.md, with the full body as written. |
import:
codex:
shred: false
lint
Budgets for the text each target loads in every session. Past a budget, agnostic-ai lint warns, and lint --strict exits 1.
| Key | Default | Effect |
|---|---|---|
instructions-words | 2000 | Words one target loads every session: entry-point file, always-on rule files, and skill and agent descriptions (LINT011). |
description-chars | 1024 | Characters in one skill or agent description (LINT012). A skill description past 1024 still warns under a higher value. |
codex-chain-bytes | 32768 | Bytes of AGENTS.md Codex reads for one directory: the root file plus each scoped file down to it, review sections included (LINT011). |
lint:
instructions-words: 3000
description-chars: 500
codex-chain-bytes: 24576
The defaults follow vendor limits: Claude Code's 200 lines for CLAUDE.md, and the Agent Skills specification for descriptions. Whatever the word budget, lint also warns when text passes Codex's 32 KiB AGENTS.md cap and Antigravity's 24,000-byte rule cap.
A missing key or 0 keeps the default. A negative value fails as AAI-004. lint in the CLI reference shows the finding. The global home config accepts the key too.
doctor
doctor.check-references.ignore
Link destinations doctor --check-references never reports as broken, whatever is on disk.
doctor:
check-references:
ignore:
- url # exact destination, as written in the source Markdown
- gcp-url
- "*-placeholder" # glob; `*` stays inside one path segment
An entry is the destination text as written, such as url for a placeholder link [Logs](url), or a path.Match glob. Matching follows the same rules as sync.unmanaged.
on-unsupported
This applies when an adapter receives a spec kind it doesn't support, such as hooks for Cursor or mcps for Cline.
| Value | Behavior |
|---|---|
warn | Default. Log to stderr and continue. |
error | Fail the sync. |
silent | Skip without logging. |
Any other value fails with AAI-004.
This policy also covers imported Claude hook root references that can't be translated, such as exec-form placeholders and complex shell expansions. See project-root paths.
Coverage notes
sync prints a note: line when specs of a kind exist but a target doesn't emit them. Either the target needs an opt-in key you haven't set, or it has no output for them:
note: 1 agent reaches warp only via outputs.warp.workflows-dir
Setting the named key clears the note. Repeated warnings collapse into one count line. sync -v shows them all.
| 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. |
coverage
on-unsupported doesn't cover coverage notes, and error never fails on them. Two other keys handle notes. Both are project-only. sync --global ignores a coverage: key in the home config and warns about it.
coverage:
fail-on-notes: true
accept:
- target: codex
kind: agents
field: tools
reason: Codex limits come from sandbox_mode; tools stays portable for other targets.
- target: [codex, gemini, cursor]
kind: skills
field: argument-hint
reason: Only Claude Code shows the hint.
coverage.fail-on-notes
Default false. When true, sync fails on each coverage note that names a target and is not accepted. It prints the notes and rolls back the writes. Under -q, the failing notes still print on stderr.
This applies to sync, sync --dry-run, sync --watch, sync --json, sync --check, sync --check --json, and sync --plan.
coverage.accept
Some notes describe a choice the project already made. For example, an agent keeps a portable tools list for Claude Code, while Codex gets its limits from x-codex.sandbox_mode. List such a note under coverage.accept with the reason. Each entry names one note with exactly one of field, via, or surface.
| Field | Required | Description |
|---|---|---|
target | yes | One target name, or a list. The entry applies to each. A name must be a built-in target or listed in targets. |
kind | yes | agents, skills, rules, hooks, mcps, commands, settings, reviews, environments, or ignores. |
field | one of three | Matches "<field> on N specs has no effect on <target>" notes for that field, whatever reason the target gives. Copilot's four matcher notes on hooks all match field: matcher. |
via | one of three | Matches "N specs reach <target> only via <via>" notes, and "only in the source dir (<via>)" notes, with that exact text. |
surface | one of three | Matches "N specs reach <target> but not <surface>" notes with that surface. |
reason | yes | Why the note is expected. sync -v prints it. |
An accepted note:
- no longer prints on
sync, andfail-on-notesignores it. - appears in
sync -vasaccepted:, with its reason on the next line. - counts in
doctor, and indoctor --jsonascoverage_accepted.
Project notes cover the setup as a whole, not one spec kind. Some start with a target name, such as A failure that lint warns with LINT024 when an entry matches no note on one of its targets. A stale entry then shows up once the target supports the field. Config loading fails on an unknown target, a repeated entry, or an entry with no field, via, or surface.What coverage.accept cannot match
note: codex: outputs.codex.config.notify is not written. coverage.accept cannot match them, and fail-on-notes does not fail on them.on-unsupported: error raises while emitting is not a note either. Examples are a Claude model name on another target, or a rule scope a target cannot keep. An entry does not stop it, even when the same entry accepts the note that warn prints.
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. |
commit | empty | Kinds of generated output to keep in Git, for every target or as <target>:<kind> for one. The block leaves out their paths. |
allow | empty | Gitignore globs written unchanged as ! lines at the end of the block, so a hand-written file at a generated path (such as a testdata/AGENTS.md fixture) is not ignored. |
worktree-include | true | With claude in targets, keep the same block in .worktreeinclude, so Claude Code copies the ignored outputs and the local layer into each worktree it creates. |
ignore-worktree-include | false | Keep managing .worktreeinclude and list it in the managed ignore block. Requires worktree-include: true and claude in targets. |
sync --gitignore and init --gitignore override it per run. See the CLI reference.
Use commit to keep in Git the files that review bots and a fresh worktree read before any sync runs:
gitignore:
enabled: true
commit: [instructions, hooks]
- It accepts
instructions,agents,skills,commands,hooks,mcps,settings,reviews,environments, andignores. An unknown kind fails config loading. instructionscovers entry-point files (AGENTS.md,CLAUDE.md,GEMINI.md) and rule outputs, root and scoped. Every other kind is named after its spec source.- A file written by several kinds, such as
.claude/settings.json, is committed when any of them is listed. - A file that only the config or an overlay produces belongs to no kind and stays ignored.
Prefix a kind with a target to commit it for that target only. This fits a cloud agent or review bot that reads its files from Git, while local tools regenerate theirs:
gitignore:
enabled: true
commit: [cursor:reviews, cursor:environments]
This keeps .cursor/environment.json, .cursor/worktrees.json, and every BUGBOT.md in Git. It ignores .claude/launch.json and .codex/environments/environment.toml. lint warns (LINT017) when the target is not in targets.
The block sits between # >>> agnostic-ai (managed) >>> and # <<< agnostic-ai (managed) <<<. Lines outside it are kept.
Claude Code worktrees
Claude Code builds a worktree from a checkout with no gitignored files. That covers a CLI --worktree, a subagent's worktree, and a Desktop worktree. Desktop runs no WorktreeCreate hook. Claude Code then copies the gitignored files that .worktreeinclude lists from the main checkout.
So with claude in targets, sync also keeps the block in .worktreeinclude, minus .sync-state, .command-lock, Claude worktree directories, and the task lock. The local layer and packs come along, so the worktree renders what the main checkout does. In the worktree, the first sync --keep-edits rewrites a copied output that its specs no longer match.
- Set
gitignore.ignore-worktree-include: trueto keep the managed file out of Git. It then no longer appears under files to commit. If the file is already tracked, runagnostic-ai sync --untrackto remove it from the index and keep the file. - Set
gitignore.worktree-include: falseto manage the file yourself. Sync then removes only its own block.
A fresh clone or git worktree lacks these paths until sync runs. See checkout and merge hooks.
Block contents
- Entries are root-anchored (
/AGENTS.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.jsonstay visible. - A per-kind dir such as
outputs.<target>.rules-dircollapses at the dir itself. - Output under a rule or review
scopestays one line per file (/services/api/AGENTS.md), so new files in that directory are not ignored. - The block always holds
agnostic-ai.local.yaml,/.agnostic-ai/.command-lock,/.agnostic-ai/.sync-state,/.agnostic-ai/packs/, and/.agnostic-ai/local/.initseeds them even withgitignore.enabled: false.init,sync, orpacks addmoves old loose copies into the block. - A partial sync keeps every configured target's entries. When
.sync-statelacks the skipped targets' outputs, as in a fresh clone, sync renders those targets in memory to list them. - Kept orphans stay ignored until removed, even when a deleted spec narrows the emitted paths.
- A target can add entries of its own, such as Claude Code's local settings.
Watched inputs
sync --watch re-emits when any of these change: the config files, any sources directory, .agnostic-ai/local/, or .agnostic-ai/overlays/. Overlays hold keys the spec layer does not own, such as Claude statusLine or Codex [history]. See sync --watch.
Path semantics
- Relative
sourcesandoutputspaths start at the directory holdingagnostic-ai.yaml. Absolutesourcespaths are used as written bysync,import, andvalidate, including watch mode. - Sync creates output directories as needed and overwrites existing files.
Entry-point files
sync copies .agnostic-ai/AGNOSTIC_AI.md into one root entry-point file per enabled target. See the per-target table. An ignored .agnostic-ai/local/AGNOSTIC_AI.md extends that body on one machine.
Write the instructions every tool shares in AGNOSTIC_AI.md. Every session loads this text, so keep it to what an agent can't infer from the code. Rules, agents, and skills stay in their own sources folders. Sync keeps whatever you write here. It overwrites only the root entry points.
When the file is missing, sync seeds it with one line saying the tool files are generated from Setting Sync skips the pointer body only when .agnostic-ai/, plus a placeholder comment. Projects created by an earlier release may still hold the old default text, a long description of agnostic-ai. doctor points it out. Replace it with your own instructions.Legacy merged rules file
outputs.<target>.rules-file: <path> restores the legacy layout: the adapter writes one merged document at <path>. Two adapters writing different content to one path fail unless you set sync.collision-policy: prefer-spec.<path> is the target's own entry-point file (outputs.claude.rules-file: CLAUDE.md). Any other path keeps the entry-point file. For claude, the pointer body also gains an @<path> import.
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.
validateflags a fence naming an unknown target or one that reads no entry-point file.import <tool>keeps a fenced source when the imported file equals what sync renders (defaultpassthrough). Otherwise it overwrites the source and warns.
Precedence
Last wins:
- Built-in defaults
agnostic-ai.yamlagnostic-ai.local.yaml- CLI flags (such as
agnostic-ai sync -t claude)
Layered specs
Specs load from four layers, lowest first. A higher layer overrides a spec of the same kind and name. New names append. The project-user layer merges into the shared spec field by field. See local overrides. agnostic-ai list shows each spec's layer.
| Layer | Root | Loaded when |
|---|---|---|
builtin | bundled specs | enabled with builtins |
| 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. .agnostic-ai/local/ stays out of Git by default and can extend AGNOSTIC_AI.md. See local overrides. $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 the paths. It works from any directory and loads no packs or project specs.
Source root: $AGNOSTIC_AI_HOME, or ~/.agnostic-ai/ when unset.
~/.agnostic-ai/
├── agnostic-ai.yaml # optional: targets, builtins, requires, lint, on-unsupported, models
├── AGNOSTIC_AI.md
├── agents/*.md
├── rules/*.md
├── hooks/*.yaml
├── settings/*.yaml # default model and effort
├── mcps/*.yaml # user-level MCP servers
├── skills/<name>/SKILL.md
└── local/ # optional personal layer, same layout
Specs in local/ merge into shared specs of the same kind and name, field by field. A ::parent line extends the shared body. See local overrides. New names append. local/AGNOSTIC_AI.md comes last in the managed instructions block. Add /local/ to the source root's .gitignore before adding personal files.
To sync a fixed set of tools without --only on every run, list them in the source root's agnostic-ai.yaml:
targets: [claude, codex, cursor]
sync --global,lint --global, andvalidate --globalthen use only those targets. Atargetslist inlocal/agnostic-ai.yamlreplaces the shared one.builtinsenables bundled specs before theglobalandglobal-locallayers. A local list replaces the shared list.--onlyand--exceptnarrow the list for one run and must name configured targets.--targetreplaces it and skips the home config'stargets.- A target with no user-level surface, such as
aiderorcontinue, is skipped with one warning. An unknown name stops the run. requiresstops the--globalcommands on an older binary.local/agnostic-ai.yamlreplaces the shared value.lintsets the budgetslint --globaluses.on-unsupportedsets whatsync --globaldoes with a skill line that Claude Code expands and another target reads as plain text:warnprints a note,errorfails the sync,silenthides it. See Claude Code body syntax.local/agnostic-ai.yamlreplaces the shared value.modelsnames the tiers global specs use, somodel: strongresolves per target as in a project. A tier inlocal/agnostic-ai.yamlreplaces the same-name shared tier whole, as in a project.lint --globalchecks the home tiers. When a file's tiers do not load,sync --globalstops. With-t, it warns instead, skips only that file's tiers, and omits themodelof any spec that names one of them. A tier name never reaches a tool as a model id.- Other keys except
versionprint a warning and are ignored. - A target dropped from the list keeps its synced files and ownership records until you remove them by hand.
Run agnostic-ai list --global to see effective specs with their builtin, global, or global-local layer. Run validate --global and lint --global to check before a sync writes. Run migrate --global to rewrite old spec forms there. Global layers never merge with project specs. Local overrides compares this layer with the project one.
- Accepted
syncflags are in the CLI reference. - Nested rules, rules with scope, path, glob, or target conditions, commands, settings
permissionsrule lists (onlypermissions.default-modeis written), inheritance, and merging with project specs are unsupported. - Skills render native frontmatter and copy bundled assets verbatim. Claude resolves skill
modelandeffort, including per-target maps andx-claudeoverrides. Shared directories such as~/.agents/skills/keep neutral frontmatter. - Codex skills also get
agents/openai.yaml, sodisable-model-invocation: truekeeps a skill manual-only there. Targets whose copy stays model-invocable print a coverage note. - Hooks and skills honor
target,targets, andtargets-exclude. Set hook events per target. Sync doesn't translate event names. - Eighteen targets have global agent output. See global output. Others warn and skip agents.
readonly: truemaps to Claude'sdisallowedTools. Codex agents keep the session sandbox and get a coverage note. - Output is real files, never symlinks. A user file that is itself a symlink, such as a dotfiles-managed
CLAUDE.md, is written through. Removing a spec behind such a symlink removes the link and the file it points at. A symlink inside a skills, agents, or rules directory stops the run. - Ownership is recorded per target in
$AGNOSTIC_AI_HOME/state/global.json. Sync keeps unrelated content and removes only recorded artifacts for the targets in the run, so--onlynever sweeps another target. - A managed hook or hooks file gone from disk is written again with a warning. A managed hook with the same matcher and command but other edits stops the run.
- A hand-written hook that exactly matches a source hook satisfies it and is never copied, recorded, or removed. One with the same matcher and command but other settings stops the run, even inside a group.
- An unrecorded file that holds exactly what sync would write is adopted. A file that differs by one byte stops the run.
- These stop the run before writes: an unmanaged agent, skill, or rule collision, a damaged marker, invalid native JSON, corrupt state, or state recorded under another
HOME. - A hand edit to a file sync owns, or to an instructions file's managed block, stops the run and names the file. Move the edit into the source, or rerun with
--backupto overwrite it and keep<path>.bak. Text outside the managed block never counts. - Without
--only, explicit targets, or a hometargetslist, a target with a relative root variable or an invalid agent name is skipped with a warning. Naming the target makes it an error. - 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. See shared names. Sync Goose and OpenHands together to update their shared agent file.
Ordinary agnostic-ai sync does not load ~/.agnostic-ai/ specs. It reads only their names, to warn about shared names.
Inside the global source root or below it, sync stops before any write and points you to sync --global. init, import, new, packs, cleanup, revert, and install-hook stop the same way. Read-only commands such as lint, validate, and doctor still run there. A path through a symlink counts. When AGNOSTIC_AI_HOME is your home directory itself, only that directory is guarded. Put project-only defaults in a project's .agnostic-ai/ or a pack.
For a home kept in Git, install-hook --global writes a pre-commit hook that runs lint --global --strict, validate --global, and sync --global --check. To start a home from what your tools already hold, run agnostic-ai import --global. See import.
Shared names
A project skill or agent can share its name with one in the home. Sync writes both, and each tool decides which one it loads. sync and doctor in the project print one warning per shared name. The warning names each project target where one copy hides the other, which copy wins, and how to fix it:
! .agnostic-ai/skills/gh-issue/SKILL.md: skill "gh-issue" also exists in ~/.agnostic-ai/skills/gh-issue/SKILL.md with different content; claude loads the global one, which exists only on this machine, gemini loads this one; to load both, rename the global one (such as gh-issue-personal), or delete it to drop it, then run `agnostic-ai sync --global`
The home exists only on your machine. A global copy that wins changes what you run, not what others run from the repository.
- When only project copies win, the warning says the global one is unused in this project.
- When both copies have the same frontmatter (apart from
name), body, and skill files, every target loads the same content. The warning says to delete one copy before they drift apart. - When a copy names a
models:tier (each side resolves it through its own config), or has a skill file sync cannot read, the warning makes no claim about content.
| Target | Skill with the same name | Agent with the same name |
|---|---|---|
| Amp | Global wins when both sync amp: ~/.agents/skills/ masks .agents/skills/ (skills) | No global agents |
| Claude Code | Global wins (skills) | Project wins (subagents) |
| Codex | Both can appear in skill selectors, so no warning (skills) | Not checked |
| Gemini CLI | Project wins (skills) | Not checked |
Other targets document no precedence, so sync does not warn for them.Which pairs the check compares
targets write. It skips a spec whose target, targets, or targets-exclude leaves one of them out.
To layer on purpose, give the project spec its own name. For example, keep a general gh-issue skill in the home, and add a project gh-issue-project skill with only this repo's branch names and checks. Both then load everywhere, and the project one can point at the global one.
To keep one name in both places, list it in sync.allow-global-names. sync stops warning about it, and doctor marks it as allowed. To stop the warning for every shared name, set sync.global-name-clash: ignore.
Default model and effort
Settings specs in the home set each tool's default model and effort in its user settings file. model and effort each take one string for every target, or a per-target map with an optional default:
# ~/.agnostic-ai/settings/defaults.yaml
model:
claude: opus
codex: gpt-6-luna
effort:
claude: high
codex: high
| Target | File | Keys |
|---|---|---|
| codex | ~/.codex/config.toml | model, model_reasoning_effort |
| claude | ~/.claude/settings.json | model, effortLevel, permissions.defaultMode |
| copilot | ~/.copilot/settings.json | model, effortLevel |
| qoder | ~/.qoder/settings.json | model.name, model.reasoningEffort |
| gemini | ~/.gemini/settings.json | model.name |
Other targets and global permission lists raise a coverage note. Augment has no default model or effort key but takes x-augment keys, such as shell. Gemini has no default effort key, so effort raises a note there.
Accepted effort values:
- Claude and Copilot:
low,medium,high,xhigh. - Qoder:
disabled,off,none,low,medium,high,xhigh,max. - Codex: any string.
A dotted key is a nested JSON object. Sync sets name inside model and leaves the other keys alone. lint --global (LINT014) and validate --global flag a value a target cannot take.
Set Claude Code's starting permission mode in a home settings spec:
# ~/.agnostic-ai/settings/defaults.yaml
permissions:
default-mode: acceptEdits
sync --global --only claude writes permissions.defaultMode in ~/.claude/settings.json, or under CLAUDE_CONFIG_DIR. It accepts default, manual, acceptEdits, plan, auto, dontAsk, and bypassPermissions.
- A later settings spec wins.
- Sync owns only this key, so hand-written
allow,deny, andaskrules stay. - Removing the field removes the managed mode, unless you changed it by hand.
Other targets report a coverage note. For Codex, set x-codex.approval_policy and x-codex.sandbox_mode instead. Claude's auto and bypassPermissions need user, managed, or session settings. Project settings can't enable them. See Claude's mode reference.
Each layer overrides the one before it:
settings/*.yamlin the home.local/settings/*.yaml. A same-named file merges into the shared one field by field. A new file comes after the shared ones, so itsmodelandeffortwin.- The project tier: a project's settings file, such as
.codex/config.tomlor.claude/settings.json, wins through the tool's own precedence. - An agent's own
modelandeffort, for that agent. - The tool's flag for one run, such as
codex -morclaude --model.
Sync edits only the keys it writes and records them in state/global.json. Every other line stays exactly as it was. Removing the spec removes only those keys. A file sync created is deleted once nothing is left in it.
- A key that already holds the value sync would write is adopted.
- A key with another value stops the run and names the file, key, and both values. This is normal for Codex, whose
/modelpicker saves toconfig.toml. The message prints the line to put in the spec.--backupoverwrites the key and keeps<path>.bak. --dry-runlists each key a write sets or removes, and--checkfails on a changed key.- An
x-<target>block sets that target's own keys in the same file. Nested objects merge leaf by leaf. Anx-claudekey wins over the portable field it shares a key with. A later spec wins key by key, andnulldrops an earlier key. - Global sync writes Codex top-level scalars and arrays, such as
x-codex.notify, to~/.codex/config.toml. Tables such asprofiles,x-claude.hooks, andx-claude.permissionsraise a coverage note. - Project sync does not route
x-codexinto.codex/config.toml. It raises a coverage note instead. agnostic-ai explain --global settings/defaults.yamlnames the file and key each target gets.
MCP servers
MCP specs in the home's mcps/ install each server in the user MCP file of every target that has one, rendered as the target's project MCP file renders it:
| Target | File | Where |
|---|---|---|
| claude | ~/.claude.json ($CLAUDE_CONFIG_DIR/.claude.json when set) | top-level mcpServers.<name> |
| codex | ~/.codex/config.toml | [mcp_servers.<name>] table |
| cursor | ~/.cursor/mcp.json | mcpServers.<name> |
| copilot | ~/.copilot/mcp-config.json | mcpServers.<name>, with tools: ["*"] when the spec sets none |
| gemini | ~/.gemini/settings.json | mcpServers.<name> |
| qoder | ~/.qoder/settings.json | mcpServers.<name> |
| augment | ~/.augment/settings.json | mcpServers.<name> |
| openhands | ~/.openhands/mcp.json ($OPENHANDS_PERSISTENCE_DIR/mcp.json when set) | mcpServers.<name>: {command, args, env} or {url, transport, headers, auth} |
Each server follows the per-key rules above:
- A hand-written server that means the same as the spec is adopted.
- A different one with the same name stops the run.
--backupoverwrites it. - A server sync wrote is removed when its spec goes. Servers you add under other names stay.
- In
~/.claude.json, sync edits only its ownmcpServersentries and keeps every other key and the file's permissions. It creates a missing file at0600. If the file changed after sync read it, sync stops without writing. Run it again. - A spec with
disabled: truestays out of the Augment, Claude, Cursor, Copilot, and OpenHands user files, where a listed server is live in every project. - When
CLAUDE_CONFIG_DIRor another root variable moves a file, the next sync removes its entries from the old one.
For a personal agent shared by Claude Code and Codex, create ~/.agnostic-ai/agents/reviewer.md:
---
name: reviewer
description: Review code for correctness
targets: [claude, codex]
---
Review the changes and report actionable findings.
Then run from any directory:
agnostic-ai sync --global --only claude,codex
agnostic-ai sync --global --only claude,codex --check
Sync writes ~/.claude/agents/reviewer.md and ~/.codex/agents/reviewer.toml. If a hand-copied file sits at an output path, save its edits in the source and move it aside first. --backup does not bypass unmanaged collisions.