Spec format
Each AI coding tool keeps its configuration in its own place and shape: .claude/agents/, .cursor/rules/, .codex/config.toml, .gemini/settings.json. A spec is one file under .agnostic-ai/ that says what you want, once. agnostic-ai sync writes it into every tool you enable, in that tool's native format.
- One source to review. A new rule or permission is one diff, not one per tool.
- No drift.
sync --checkfails when a native file no longer matches its spec. - Free to switch tools. Adding a tool is one entry in
targets:, not a rewrite. - Visible gaps. When a tool cannot hold a field, sync prints a coverage note instead of dropping it silently.
Folders
Each folder holds one spec kind. Pick the one for what you want the agent to do.
| Folder | Holds | Use it when |
|---|---|---|
agents/ | Subagents with their own prompt, tools, and model | a task deserves a specialist with a narrow role |
skills/ | Procedures loaded on demand, with bundled files | a workflow is long, occasional, or needs scripts |
rules/ | Standing conventions, for the project, a directory, or a file pattern | the agent must follow it every time |
commands/ | Slash commands | a person starts the workflow by name |
hooks/ | Commands the tool runs on lifecycle events | something must happen every time, not when the model remembers |
mcps/ | MCP server connections | the agent needs an outside tool or data source |
settings/ | Permissions, default model, and effort | one policy should decide what agents may run |
reviews/ | Guidance for code-review bots | a review bot should check project rules |
environments/ | Worktree setup, dev servers, and cleanup | agents work in fresh worktrees |
ignore/ | Paths the agent must not read | secrets or build output must stay out of context |
overlays/ | Native settings and helper files that import captured | a tool setting has no portable spec |
Two more entries sit next to them:
AGNOSTIC_AI.mdis the instruction body every entry-point file carries, such asAGENTS.mdandCLAUDE.md. See entry-point files.local/holds personal specs that stay out of Git. See local overrides.
Paths are relative to .agnostic-ai/ by default, so rules/*.md means .agnostic-ai/rules/*.md. Override the directories with sources. The capability matrix shows which targets get each kind. Each target page shows how that tool renders it.
agnostic-ai new <kind> <name> scaffolds an agent, skill, rule, hook, or MCP server. Getting started walks through a first rule.
Discovery is recursive:
- Every
.mdunderagents/,skills/,rules/,commands/,reviews/, andignore/loads. - Every
.yamlunderhooks/,mcps/,settings/, andenvironments/loads.
Nested layout: per-directory scope
A spec in a subdirectory of its source dir gets an implicit scope equal to that subpath.
rules/
├── conventional-commits.md # scope: "" (root)
└── backend/
├── auth.md # scope: "backend"
└── api/limits.md # scope: "backend/api"
For rules, scope controls native activation or directory discovery. A rule can set scope: services/payments, which wins over its folder under rules/. A folder scopes a rule only when it names a project directory. agnostic-ai new rule payments-context --scope services/payments creates one.
Scoped bodies stay out of root instruction appendices. Supported targets get native path conditions or a nested instruction file. Unsupported targets skip the rule with a warning, or fail under on-unsupported: error. Directory-specific instructions has the target matrix and selector limits.
Target scoping
Four fields limit where a spec of any kind emits.
| Field | Effect |
|---|---|
target | Emit only to this one target. |
targets | Emit only to these targets. |
target-exclude | Emit everywhere except this target. |
targets-exclude | Emit everywhere except these targets. |
With none set, the spec emits to every target that supports its kind. target beats targets. Exclusion wins over inclusion.
Import auto-scoping
A hook imported from a tool gets target: <tool> (codex, claude, or gemini). Delete the field to let it reach every target.
import claude and import codex do the same for agents and skills. When both .claude/ and .codex/ exist but only one has a spec, it gets target: <tool>. A spec in both stays unscoped. So does every spec in a single-tool project, so round-trips stay byte-identical.
Frontmatter rules
- YAML between two
---lines at the top of the file. - Empty frontmatter (
---\n---\n) means no metadata. - A file without frontmatter still loads; its name defaults to the filename.
- Malformed frontmatter counts as no metadata, and the whole file becomes the body.
- Fields not listed on a kind's page pass through on emit.
Per-target body fences
To vary prose per target, wrap the part that differs in ::target fences. Content outside a fence emits everywhere. Content inside emits only to the listed targets. Marker lines never reach the output.
Shared intro paragraph.
::target claude
Claude-only section.
::end
::targets codex gemini
Codex and Gemini section.
::end
| Syntax | Meaning |
|---|---|
::target <name> | Opens a fence for one target. |
::targets <a> <b> | Opens a fence for several targets. |
::end | Closes the most recent fence. A missing ::end runs to the end of the body. |
importround-trips keep fences intact.import codexbuilds them when Claude and Codex ship the same agent or skill with different bodies.- Fences also work in
.agnostic-ai/AGNOSTIC_AI.md. A block reaches an entry-point file when any target that reads the file is listed. The whole family sharesAGENTS.md, so::target codexcontent reaches every reader. A shared file is never split. See Entry-point files.
Path variables: {{$NAME}}
A spec body can name a directory without hardcoding one target's layout. {{$SKILLS_DIR}} expands to .claude/skills for claude, .agents/skills for codex, and .github/skills for copilot.
| Variable | Resolves to |
|---|---|
{{$SKILLS_DIR}} | the target's skills directory |
{{$AGENTS_DIR}} | the target's agents directory |
{{$COMMANDS_DIR}} | the target's commands directory |
{{$RULES_DIR}} | the target's rules directory |
{{$MCP_FILE}} | the target's MCP config file |
- Bodies only. Frontmatter values do not expand.
- An
outputs.<target>.<field>override wins. Withoutputs.claude.skills-dir: custom/skills,{{$SKILLS_DIR}}follows it. - A variable with no target surface stays verbatim and raises a coverage note. aider and jules resolve no variables.
- A shared file resolves for every tool that reads it. This covers the rules block of an entry point, the review section of
AGENTS.md, and a nestedAGENTS.md.GEMINI.mdhas one reader, so it gets.gemini/skills. Codex and Amp both readAGENTS.mdand use.agents/skills, so it expands. Codex and OpenCode use different paths, so the token stays verbatim and raises one coverage note naming the file. - Only where the target has a dedicated surface. Targets that flatten agents into rules (continue, trae, windsurf) or commands (gemini) declare no
{{$AGENTS_DIR}}. Antigravity, Goose, and OpenHands resolve it to.agents/agents. - The
$sigil is required. Plain{{placeholder}}stays untouched, so Warp workflow arguments and Handlebars or Jinja survive. Lowercase names such as{{$skills_dir}}do not resolve.
Agent and skill references
A spec body can name another agent or skill by how each target invokes it. {{$AGENT:reviewer}} and {{$SKILL:commit}} render that target's documented phrase.
Before pushing, run {{$AGENT:reviewer}}, then {{$SKILL:commit}}.
| Target | {{$AGENT:reviewer}} | {{$SKILL:commit}} | Source |
|---|---|---|---|
| claude | the reviewer subagent | /commit | subagents, skills |
| codex | the reviewer agent (neutral) | $commit | skills |
| cursor | the reviewer subagent | /commit | subagents, skills |
| copilot | the reviewer agent | the /commit skill | custom agents, skills |
| every other target | the reviewer agent | the commit skill |
- Bodies only, with the
$sigil and an uppercase keyword, like path variables.{{AGENT:reviewer}}stays untouched. - A target with no documented form renders a neutral phrase and raises one coverage note. Unlike a path variable, the token does not stay verbatim: a raw
{{$AGENT:reviewer}}reads worse to a model than plain words. - A rule or review in a shared document, such as the rules block of
AGENTS.mdor a nestedAGENTS.md, renders the neutral phrase, because several tools read that file. Codex reads its rules there, so a Codex rule always gets the neutral phrase. The same goes for a skill, agent, or command in a directory two enabled tools write, such as.agents/skillsfor Codex and Windsurf. - An unknown name fails
lintwith LINT033, which lists the known agents or skills. So does a name whose spec does not sync to every target the referring spec reaches. Only project specs count: a teammate without your global agent would get a phrase that points at nothing.
Target-specific extensions: x-<target> namespace
Use an x-<target>: block for fields only one adapter reads. Other adapters strip it, so the spec stays portable.
---
name: code-reviewer
description: Reviews diffs.
model: sonnet
x-claude:
allowed-tools: [Read, Grep, Bash]
x-cursor:
globs: "src/**"
alwaysApply: false
---
For each target, sync drops all x-* keys. Then it flattens the matching x-<target> block into the top level, overriding keys with the same name.
| Target | Resulting frontmatter |
|---|---|
claude | name, description, model, allowed-tools |
cursor | name, description, model, globs, alwaysApply |
gemini | description (name becomes the .toml filename; model is not emitted for commands) |
Arbitrary custom keys
Any other key under x-<target> emits verbatim into that target's output, in sorted order, and never leaks across targets.
- Validate the keys against the target's schema yourself. Each target page lists the keys its adapter manages.
- A target with no surface for a spec kind drops custom keys for that kind.
- Gemini TOML accepts only a string, bool, number, or string array, and skips nested tables.
On a settings spec, the block merges into the target's settings file by the rules in settings.