| Kind | Source | Format |
|---|---|---|
| Agent | agents/*.md |
Markdown + YAML frontmatter |
| Skill | skills/*.md or skills/<name>/SKILL.md |
Markdown + YAML frontmatter |
| Rule | rules/*.md |
Markdown + YAML frontmatter |
| Hook | hooks/*.yaml |
YAML |
| MCP | mcps/*.yaml |
YAML |
| Command | commands/*.md |
Markdown + YAML frontmatter |
| Settings | settings/*.yaml |
YAML |
| Review | reviews/*.md |
Markdown + YAML frontmatter |
| Environment | environments/*.yaml |
YAML |
| Ignore | ignore/*.md |
Markdown + YAML frontmatter |
Discovery is recursive. Every .md under agents/, skills/, rules/, commands/, reviews/, ignore/ is picked up; every .yaml under hooks/, mcps/, settings/, and environments/.
A spec in a subdirectory of its source dir carries an implicit scope equal to that subpath.
rules/
├── conventional-commits.md # scope: "" (root)
├── backend/
│ └── auth.md # scope: "backend"
└── backend/api/
└── limits.md # scope: "backend/api"
Adapters that produce per-directory output honor the scope:
| Target | Scoped layout |
|---|---|
cursor |
<scope>/.cursor/rules/<name>.mdc |
cline |
<scope>/.clinerules/<name>.md |
windsurf |
<scope>/.windsurf/rules/<name>.md |
continue |
<scope>/.continue/rules/<name>.md |
copilot |
<scope>/** glob on one .github/instructions/<name>.instructions.md (no nested dirs) |
Single-document targets (claude CLAUDE.md, aider CONVENTIONS.md) merge regardless of scope. Source-dir targets (codex, gemini, amp, warp) reference rules from the spec dir and no longer emit per-directory scoped files (e.g. src/AGENTS.md) by default. The scope stays in the source provenance comment (<!-- source: rules/backend/auth.md -->).
A frontmatter scope: field is also accepted as a fallback when moving the file is impractical (a single rule scoped to a subtree).
---
name: code-reviewer
description: Reviews diffs for bugs, style, and architectural issues.
tools: [Read, Grep, Bash]
model: sonnet
---
You are a code reviewer. Examine the diff for:
- Logic bugs and edge cases
- Style consistency
- Security issues
Report concise findings with `file:line` references.| Field | Required | Default | Description |
|---|---|---|---|
name |
no | filename without .md |
Agent identifier. Used for the output filename. |
description |
no | empty | One-liner shown in tool listings. |
tools |
no | unset | Tools the agent may invoke. Format depends on target CLI. |
model |
no | unset | Preferred model. String applies everywhere; a map selects per target (see below). |
Any other frontmatter field passes through to the target CLI unchanged.
model: accepts a string (same model everywhere) or a map keyed by target name with an optional default fallback.
---
name: code-reviewer
model:
claude: opus
codex: gpt-5.5
default: gpt-4o
---Resolution per target: matching key wins, else default, else no model line is emitted (the CLI uses its own built-in default). An x-<target>.model override beats the map; x-<target>.model: null deletes it.
| Want | Write |
|---|---|
| Same model everywhere | model: sonnet |
| Per-target, with a concrete fallback | model: {claude: sonnet, default: gpt-4o} |
| Per-target, native default elsewhere | model: {claude: sonnet} |
Two layouts:
Flat: skills/yaml-validator.md
Nested (for skills with attached resources):
skills/yaml-validator/SKILL.md
skills/yaml-validator/schema.yaml
Only SKILL.md and flat skills/*.md are parsed as skills. Every other file inside a nested skill directory (scripts, templates, fixtures, subdirectories, and extra *.md such as examples.md) is a bundled asset: it copies verbatim to the same relative location under each target's skills dir and is never promoted to its own skill. Ship a check.mjs, templates/*.tpl, or fixtures/*.json the skill body references. Executable bits are preserved both directions through import + sync.
---
name: yaml-validator
description: Validate YAML against a schema.
---
# YAML Validator
## Steps
1. Read target file
2. Parse YAML
3. Compare against `schema.yaml`
4. Report violations as `path: message`| Field | Required | Default | Description |
|---|---|---|---|
name |
no | dir or filename | Skill identifier. Used for the output directory. |
description |
no | empty | One-liner shown when the model decides whether to invoke the skill. |
Emission by target:
- Native (
<dir>/<name>/SKILL.md, one folder per skill): Claude Code (.claude/skills/), Codex (.codex/skills/), Amp (.agents/skills/), Antigravity (.agent/skills/). - As a rule file (
skill-<name>.{mdc,md}): Cursor, Cline, Windsurf, Continue. - Listed by default, opt into native slash commands via
outputs.<target>.emit-skills-as-commands: true: Gemini, OpenCode. - Listed in a
## Skillssection: Aider, Copilot, Zed, Warp.
See targets for the full matrix.
---
name: conventional-commits
description: Always use Conventional Commits format.
globs: "**/*"
alwaysApply: true
---
Use `feat:`, `fix:`, `docs:`, etc. Subject under 72 chars.| Field | Required | Default | Description |
|---|---|---|---|
name |
no | filename | Rule identifier. |
description |
no | empty | Short summary. |
globs |
no | **/* |
File patterns where this rule applies. Used by Cursor. |
alwaysApply |
no | true for rules, false for agents emitted as Cursor rules |
Inject unconditionally, or only on matching context. |
Pure YAML, no markdown body.
name: format-on-save
description: Run formatter after Edit/Write tools modify files.
event: PostToolUse
matcher: "Edit|Write"
command: "npx prettier --write \"$CLAUDE_FILE_PATHS\""| Field | Required | Default | Description |
|---|---|---|---|
name |
no | filename | Hook identifier. |
description |
no | empty | Free-form documentation. |
event |
yes | none | Hook event. See list below. |
matcher |
no | empty | Regex on tool name (or other event-specific selector). |
command |
yes | none | Shell command to run when triggered. |
target |
no | empty | Single target name. Emits only there. |
targets |
no | empty | List of target names. Emits only to those. |
target-exclude |
no | empty | Single target name to block. Emits everywhere else. |
targets-exclude |
no | empty | List of target names to block. Emits to every other configured target. |
With none of the scoping fields set, the hook emits to every target that supports hooks. target takes precedence over targets when both appear. Exclude wins: a target in both an include and an exclude list is excluded.
These four scoping fields work on every spec kind (agents, skills, rules, commands, mcps), not only hooks. A target: codex agent emits only into .codex/agents/; a targets-exclude: [gemini] skill emits to every configured target except gemini.
When a spec emits to multiple targets but the prose must diverge (codex wants a "Workflow" section claude does not), wrap the divergent prose in ::target fences. Outside-fence content emits everywhere; inside-fence content emits only to the listed targets. Marker lines never reach the output.
---
name: test
description: Run the test suite
---
# Test
Shared intro paragraph.
::target claude
## Scope mapping (Claude)
| Changed | Command |
|---------|---------|
| ... | ... |
::end
::target codex
## Workflow (Codex)
1. Choose scope
2. Run `composer test`
::end
Shared outro.::target <name>and::targets <a> <b>open a fence pinned to one or more targets.::endcloses the most recent fence.- An unterminated fence runs to end-of-body, so a missing
::endkeeps the tail of the file. - The empty target (the source view used by
importround-trips) returns the body with fences intact, so a re-emit stays byte-stable. import codexbuilds these fences automatically when both tools ship the same agent or skill name with diverging bodies: the longest common prefix and suffix stay un-fenced, and each tool's unique middle gets its own::targetblock.
Hooks imported from a tool-native source auto-set target to that tool (codex import → target: codex, claude → target: claude, gemini → target: gemini). Remove the field by hand to let the hook flow everywhere.
event: PostToolUse
matcher: apply_patch|Edit|Write
command: "$(git rev-parse --show-toplevel)/.codex/hooks/format-php.sh"
target: codex # shell-expanded codex path; do not leak to other toolsimport claude and import codex apply the same auto-scoping to agents and skills: when both .claude/ and .codex/ exist but only one carries a given spec, the captured frontmatter gains target: <tool>. A spec present in both tools stays un-scoped (cross-emit). Pure single-tool projects also stay un-scoped, so byte-identical round-trips hold.
| Event | When it fires |
|---|---|
PreToolUse |
Before any tool call. Matcher is the tool name regex. |
PostToolUse |
After any tool call. Matcher is the tool name regex. |
UserPromptSubmit |
Before the model reads a new user message. |
Stop |
When the model stops generating. |
Notification |
When Claude Code surfaces a system notification. |
Native emission: Claude Code (.claude/settings.json), Codex (.codex/hooks.json, per-event arrays), Gemini (.gemini/settings.json hooks), Cursor (.cursor/hooks.json, version + per-event arrays). Other targets log a warning and skip. Event names pass through verbatim, so a Cursor hook sets event: to a Cursor name (beforeShellExecution, afterFileEdit, beforeSubmitPrompt, sessionStart, stop, ...). See each tool's docs for its full event list and matcher semantics.
agnostic-ai emits the event: value verbatim into each target's schema; it does not translate event names between tools. Claude and Codex share the PreToolUse / PostToolUse / UserPromptSubmit vocabulary, so one hook spec feeds both. Gemini uses its own names (BeforeTool, AfterTool, SessionStart, SessionEnd), so a Gemini hook must set event: to one of those. agnostic-ai validate flags any event a target does not recognize.
This spec:
event: PostToolUse
matcher: Bash(git commit*)
command: echo "tests please"Renders to .claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash(git commit*)",
"hooks": [
{"type": "command", "command": "echo \"tests please\""}
]
}
]
}
}Renders to .codex/hooks.json (same nested shape as Claude, routed into per-event arrays):
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash(git commit*)",
"hooks": [
{"type": "command", "command": "echo \"tests please\""}
]
}
]
}
}Gemini uses different event names, so a Gemini hook sets event: to one of its own. This spec:
event: AfterTool
matcher: Bash(git commit*)
command: echo "tests please"Renders to .gemini/settings.json (flat shape, event name passed through unchanged):
{
"hooks": {
"AfterTool": [
{"matcher": "Bash(git commit*)", "command": "echo \"tests please\""}
]
}
}When command is a list, each entry becomes a separate hook entry (Claude, Codex).
Pure YAML, no markdown body. One file per server.
name: filesystem
description: Local filesystem access for the model.
type: stdio
command: npx
args:
- -y
- "@modelcontextprotocol/server-filesystem"
env:
ROOT: /tmp| Field | Required | Default | Description |
|---|---|---|---|
name |
yes | none | Server identifier. Becomes the key in the generated config. |
description |
no | empty | Free-form documentation. |
type |
no | stdio |
Transport: stdio, http, or sse. |
command |
stdio only | none | Executable to launch. |
args |
no | empty | Argument list for the command. |
env |
no | empty | Environment variables passed to the server. |
url |
http/sse only | none | Endpoint URL. |
headers |
no | empty | HTTP headers for http/sse transports. |
disabled |
no | false |
When true, passes disabled: true to targets that support it (Claude Code, Cursor, Copilot). |
roots |
no | empty | List of {uri, name} objects. Passed to targets that support MCP roots (Claude Code, Cursor, Copilot). |
Targets with native MCP propagation:
| Target | File | Schema |
|---|---|---|
| Claude Code | .mcp.json |
standard mcpServers |
| Cursor | .cursor/mcp.json |
standard mcpServers |
| Copilot / VS Code | .vscode/mcp.json |
servers with type field |
| Codex | .codex/config.toml |
[mcp_servers.<name>] table |
| Gemini | .gemini/settings.json |
mcpServers (uses httpUrl for HTTP) |
| Continue | .continue/mcpServers/<name>.yaml |
one YAML per server |
| Amp | .amp/settings.json |
amp.mcpServers (dotted key) |
| Zed | .zed/settings.json |
context_servers (stdio: command/args/env; HTTP/SSE: native url/headers) |
| Warp | .warp/.mcp.json |
standard mcpServers |
| OpenCode | opencode.json |
mcp with type: local|remote |
Aider, Cline, Windsurf, and Antigravity have no project-scoped MCP file and skip with a warning.
Markdown with optional YAML frontmatter. Each spec becomes one native slash command on supported targets.
---
name: deploy
description: Deploy the app to staging.
argument-hint: <env>
---
Deploy the app to {{env}}.
1. Run tests.
2. Build artifacts.
3. Push to the {{env}} environment.| Field | Required | Default | Description |
|---|---|---|---|
name |
no | filename | Command identifier. Becomes the slash name (e.g. /deploy). |
description |
no | empty | One-liner shown in slash-command pickers. |
argument-hint |
no | empty | Hint string shown after the command. Claude-only; passes through. |
Any other frontmatter passes through unchanged. Use the x-<target> namespace for target-specific keys (e.g. x-claude.allowed-tools).
Native emission: Claude Code (.claude/commands/<name>.md), Codex (.codex/prompts/<name>.md), Gemini (.gemini/commands/<name>.toml), OpenCode (.opencode/commands/<name>.md), and Amp (.agents/commands/<name>.md). Cursor emits commands when outputs.cursor.commands-dir is set, otherwise it prints a coverage note. Other targets log a warning and skip.
Pure YAML, one file per settings group under settings/. A tool-neutral place to single-source agent permissions and the default model instead of hand-editing each tool's settings file.
permissions:
allow:
- Bash(go test:*)
deny:
- Bash(rm:*)
ask:
- Bash(git push:*)
model: claude-opus-4-8| Field | Required | Default | Description |
|---|---|---|---|
permissions.allow |
no | empty | Permission rules auto-approved without prompting. |
permissions.deny |
no | empty | Permission rules always blocked. |
permissions.ask |
no | empty | Permission rules that prompt before running. |
model |
no | empty | Default model the tool should use. |
Multiple settings files merge: permission lists concatenate (de-duped, source order preserved); the last non-empty model wins.
Native emission: Claude Code (.claude/settings.json permissions + model). Other targets have no equivalent settings surface and report the spec as unsupported.
When the Claude-specific outputs.claude.settings config in agnostic-ai.yaml also sets these fields, scalars like model take the config value (it is the more specific source), while permissions lists are unioned across the captured overlay, the settings spec, and the config so no layer silently drops another's allow/deny/ask rules.
settings specs are hand-authored: import claude does not create them. An imported .claude/settings.json keeps its permissions and model in the Claude-only overlay (.agnostic-ai/overlays/claude.settings.json), since a value like a Claude model id does not necessarily port to other tools. Author a settings spec when you want one source to drive multiple tools; a settings spec's permissions still union with the imported overlay on sync.
Markdown with optional YAML frontmatter, one file per guidance group under reviews/. A single source for code-review-bot guidance that maps to each ecosystem's native review-rule file.
---
scope: backend
---
Flag any handler that talks to the database directly instead of going through a repository.Review specs honor scope (and the source-directory layout) exactly like rules, so per-directory guidance is supported. Specs that share a scope concatenate into that scope's single review file.
Native emission: Cursor Bugbot BUGBOT.md: the repo root for unscoped specs, <scope>/BUGBOT.md for scoped ones. Override the basename with outputs.cursor.review-file. Other targets have no equivalent review-rule file yet and report the spec as unsupported.
Pure YAML, one file per environment group under environments/. A single source for how a coding agent boots its dev env (install dependencies, start services, forward ports, open terminals).
install: go mod download
terminals:
- name: dev
command: go run ./cmd/agnostic-aiNative emission: Cursor background-agent environment.json. The spec body is the .cursor/environment.json content: every key except the agnostic-ai routing fields (name, scope, target(s), target(s)-exclude, description) passes through verbatim, so you author Cursor's schema while agnostic-ai single-sources it. Multiple environment specs merge by top-level key (last wins). Override the path with outputs.cursor.environment-file. Other targets (devcontainers, Codex setup scripts) have no emitter yet and report the spec as unsupported.
Markdown with optional YAML frontmatter, one file per group under ignore/. The body is gitignore-syntax patterns naming what an agent must not read or index. One source fans out to each tool's native ignore file.
# Secrets and build artifacts the agent should never read
*.env
secrets/
dist/Native emission (gitignore syntax, under a # provenance header): Cursor .cursorignore, Gemini .aiexclude, Aider .aiderignore. Each spec body is trimmed and the specs are concatenated with a blank line between them. Each path is overridable via outputs.<target>.ignore-file. Targets without an ignore-file convention report the spec as unsupported.
- YAML between two
---lines at the top of the file. - Empty (
---\n---\n) is allowed and treated as no metadata. - Files without frontmatter still load; name defaults to the filename.
- Malformed frontmatter is treated as no metadata; the entire content becomes the body.
- Any field not listed above passes through on emit. Useful for target-specific extensions.
Use x-<target>: blocks to attach fields only one adapter consumes. Other adapters strip the block on emit, 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
---Resolution per target:
- All
x-*keys are dropped first. - The matching
x-<target>block is flattened into top-level meta. - Flattened keys override top-level 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 has no native command surface and is not emitted) |
For Codex agents, x-codex fields (model, model_reasoning_effort, sandbox_mode, nickname_candidates) pass through to the generated .codex/agents/<name>.toml. For Codex skills, x-codex.interface, x-codex.policy, and x-codex.dependencies trigger an additional .codex/skills/<name>/agents/openai.yaml for UI customization, policy, and tool dependencies.
Any other key under x-<target> emits verbatim into that target's output surface. Declaring it under x-<target> is the opt-in: shared top-level keys stay stripped, so plain specs keep emitting valid files. Keys emit in sorted order and never leak across targets. Validate them against the target's schema yourself.
Per surface:
| Target | Surface | Custom key lands in |
|---|---|---|
claude |
SKILL.md frontmatter |
every x-claude key (e.g. disable-model-invocation: true) |
codex |
SKILL.md frontmatter |
every x-codex key except interface/policy/dependencies (those route to openai.yaml) |
copilot |
.instructions.md frontmatter |
every x-copilot key, alongside applyTo |
opencode |
command .md frontmatter |
every x-opencode key beyond description/agent/model/subtask (skills-as-commands) |
amp |
command .md frontmatter |
every x-amp key beyond description (skills-as-commands) |
gemini |
command .toml |
every x-gemini key (string, bool, number, or string array; skills-as-commands) |
Targets that emit no surface for a spec kind drop arbitrary custom keys. Gemini TOML accepts scalars and string arrays only; nested tables are skipped.