Browse documentation
DocsReference

Spec format

Source paths below are relative to .agnostic-ai/ by default. For example, rules/*.md means .agnostic-ai/rules/*.md. Override directories with sources.

Start with a rule for conventions, a skill for a reusable workflow, or an MCP server for a tool connection. See Getting started for a complete first-rule example.

KindSourceFormat
Agentagents/*.mdMarkdown + YAML frontmatter
Skillskills/*.md or skills/<name>/SKILL.mdMarkdown + YAML frontmatter
Rulerules/*.mdMarkdown + YAML frontmatter
Hookhooks/*.yamlYAML
MCPmcps/*.yamlYAML
Commandcommands/*.mdMarkdown + YAML frontmatter
Settingssettings/*.yamlYAML
Reviewreviews/*.mdMarkdown + YAML frontmatter
Environmentenvironments/*.yamlYAML
Ignoreignore/*.mdMarkdown + 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/.

Nested layout: per-directory scope

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"

For rules, scope controls native activation or directory discovery. It does not merely organize generated files. A flat rule may set scope: services/payments; source-layout scope takes precedence. Create one with:

agnostic-ai new rule payments-context --scope services/payments

Scoped bodies are excluded from root instruction appendices. Supported targets receive native path conditions or a nested instruction file. Unsupported targets skip the rule with a warning, or fail under on-unsupported: error.

See directory-specific instructions for the complete target matrix, selector rules, runtime limits, and shared-reader compatibility.

Agents

---
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.
FieldRequiredDefaultDescription
namenofilename without .mdAgent identifier. Used for the output filename.
descriptionnoemptyOne-liner shown in tool listings.
toolsnounsetTools the agent may invoke. Behavior varies by target; see tools support by target below.
modelnounsetPreferred model. String applies everywhere; a map selects per target (see below).

Any other frontmatter field passes through to the target CLI unchanged.

Per-target models

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.

WantWrite
Same model everywheremodel: sonnet
Per-target, with a concrete fallbackmodel: {claude: sonnet, default: gpt-4o}
Per-target, native default elsewheremodel: {claude: sonnet}

Skills

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

Zed skill names must use 1-64 lowercase letters or digits with single hyphens between segments. Zed sync rejects names such as `Deploy`, `my_skill`, and `my--skill` with an actionable error. Other targets retain their own naming rules.

## Steps
1. Read target file
2. Parse YAML
3. Compare against `schema.yaml`
4. Report violations as `path: message`
FieldRequiredDefaultDescription
namenodir or filenameSkill identifier. Used for the output directory.
descriptionnoemptyOne-liner shown when the model decides whether to invoke the skill.

Emission by target:

  • Native, one folder per skill at <dir>/<name>/SKILL.md, carrying bundled assets verbatim. Most targets. Several share one tree at .agents/skills/, so identical bytes dedupe instead of writing another on-disk copy.
  • Flattened to a rule file (skill-<name>.md) on the few targets with no skill surface. Bundled assets cannot follow, so those raise a coverage note.
  • Also as a slash command, opt-in per target via outputs.<target>.emit-skills-as-commands: true.

Which target does which, and the exact directory each reads, is the Skills row and cross-cutting bullet in targets — that list is kept current per change and this one is deliberately not a second copy of it.

Rules

---
name: conventional-commits
description: Always use Conventional Commits format.
globs: "**/*"
alwaysApply: true
---

Use `feat:`, `fix:`, `docs:`, etc. Subject under 72 chars.
FieldRequiredDefaultDescription
namenofilenameRule identifier.
descriptionnoemptyShort summary.
scopenoproject-wideProject-relative directory and descendants. Source-layout scope takes precedence. Native routing and supported targets are listed in scoped context.
globsnotarget-dependent; new rule seeds **/*Project-relative file patterns. With scope, the selector must preserve the directory boundary. new rule --scope omits this field.
pathsnounsetProject-relative file patterns, as a string or list. Scoped rules accept this alongside or instead of globs; see selector limits.
alwaysApplynotarget-dependent; new rule seeds trueRequests unconditional activation. With scope, applies only within the directory boundary; sync chooses the required native flags. new rule --scope omits this field.

Hooks

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\""
FieldRequiredDefaultDescription
namenofilenameHook identifier.
descriptionnoemptyFree-form documentation.
eventyesnoneHook event. See list below.
matchernoemptyRegex on tool name (or other event-specific selector).
commandcommand handlers onlynoneShell command or list. Not needed for Claude HTTP/MCP/prompt hooks, Cursor prompt hooks, a valid x-kiro.action, or a hook that sets x-gemini.hooks.
argsnoemptyArgument list. Claude Code, Qoder and Copilot. Setting it switches the hook to exec form: command is resolved as an executable and spawned directly with args as the argument vector, no shell involved, so spaces, apostrophes, $, and backticks pass through verbatim. Leave it unset for shell form, which is what you want when the command uses a pipe or &&. The targets spell the form differently. Claude Code and Qoder keep the executable in command, and Qoder ignores shell in exec form; Copilot moves it to exec and forbids carrying both, and its exec form runs under Copilot CLI only, so a hook that must also run under Copilot cloud agent leaves args unset. sync says so with a coverage note.
typenocommandClaude: command, http, mcp_tool, or prompt. Codex: command or mcp_tool. Cursor: command or prompt.
serveryes, when type: mcp_toolnoneName of the already-connected MCP server to call. Claude + Codex.
toolyes, when type: mcp_toolnoneName of the tool to call on that server. Claude + Codex.
inputno, type: mcp_tool onlyemptyJSON object of argument templates for the tool call. Claude + Codex.
urlHTTP handler onlynoneClaude or Copilot hook endpoint. Optional headers supplies HTTP headers and allowedEnvVars names variables allowed in header values.
promptprompt handler onlynoneNatural-language condition for Claude, Cursor, or Copilot. Copilot accepts it only on sessionStart. Optional model selects Claude's evaluating model.
continueOnBlockno, Claude promptfalseFeed a blocking prompt-hook reason back to Claude and continue the turn instead of ending it.
timeoutnononeSeconds before the tool cancels the hook. Claude + Codex, both shapes, Cursor, Kiro (0 disables the timeout there instead of meaning immediate cancellation; kiro.dev's own default when the key is absent is 60), Qoder (default 600 when absent), Crush (default 30 when absent), Factory (default 60 when absent), Goose (default 30 when absent), and Copilot (timeoutSec). Augment and Gemini convert this value to milliseconds before writing it (vendor default 60000 when absent).
statusMessagenoemptySpinner message while the hook runs. Claude + Codex, both shapes, and Qoder.
asyncnofalseRun in the background without blocking. Claude + Codex, and Qoder.
asyncRewakenofalseBackground run that wakes Claude on exit code 2 (implies async). Claude and Qoder.
shellnoemptybash or powershell. Claude and Qoder.
ifnoemptyPermission-rule filter (e.g. Bash(git *)) gating when the hook fires. Claude and Qoder.
loop_limitno5Stop-blocking limit. Trae: Stop only. Cursor: stop and subagentStop, with null allowing unlimited runs.
failClosedno, CursorfalseBlock the action when the hook fails. Applies to command and prompt handlers.
disablednofalseKeep the hook in the file but stop it running. Antigravity only, where it writes the definition's own enabled: false. Every other target emits the hook unchanged.
x-goose.on_failurenoallowallow or block for a Goose PreToolUse command hook that cannot complete. Goose ignores it on other events.
commandWindowsnoemptyWindows-specific command override. Codex.
additionalContextLimitnononeToken threshold for how much hook output reaches the model. Codex. Set 0 to pass the complete additional context.
targetnoemptySingle target name. Emits only there.
targetsnoemptyList of target names. Emits only to those.
target-excludenoemptySingle target name to block. Emits everywhere else.
targets-excludenoemptyList of target names to block. Emits to every other configured target.

Tool-specific fields emit only where that tool's schema defines them; other targets ignore them. Scope non-command hooks with target or targets. Claude's stable handlers preserve timeout, statusMessage, if, and once; args, async, asyncRewake, and shell stay command-only. Cursor prompt hooks preserve matcher, timeout, loop_limit (including null), and failClosed. Kiro accepts x-kiro.action: {type: agent, prompt: ...} or {type: command, command: ...} without a generic command; the explicit action replaces a fallback command list and invalid overrides fail sync.

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.

Per-target body fences

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.
  • ::end closes the most recent fence.
  • An unterminated fence runs to end-of-body, so a missing ::end keeps the tail of the file.
  • The empty target (the source view used by import round-trips) returns the body with fences intact, so a re-emit stays byte-stable.
  • import codex builds 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 ::target block.
  • The same fences work in .agnostic-ai/AGNOSTIC_AI.md. A fenced block reaches an entry-point file when any target that reads the file is listed. AGENTS.md is read by the whole AGENTS.md family (codex, amp, warp, cline, ...), so ::target codex lands there and every reader of that file sees it. A shared file is never split. See Entry-point files.

Import auto-scoping

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 tools

import 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.

Supported events (Claude Code)

EventWhen it fires
PreToolUseBefore any tool call. Matcher is the tool name regex.
PostToolUseAfter any tool call. Matcher is the tool name regex.
PostToolUseFailureAfter a tool call fails.
PermissionRequestWhen a permission dialog appears.
UserPromptSubmitBefore the model reads a new user message.
SubagentStart / SubagentStopWhen a subagent spawns / finishes.
StopWhen the model stops generating.
NotificationWhen Claude Code surfaces a system notification.
SessionStart / SessionEndWhen a session begins / terminates.
PreCompact / PostCompactAround context compaction.

Claude Code defines more events (Setup, InstructionsLoaded, TaskCompleted, TeammateIdle, FileChanged, ...); the event: value passes through verbatim, so any documented name works. See the Claude Code hooks reference for the full list. Codex shares the SessionStart/SubagentStart/UserPromptSubmit/PreToolUse/PermissionRequest/PostToolUse/PreCompact/PostCompact/Stop/SubagentStop vocabulary.

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), Windsurf / Devin CLI (.devin/hooks.v1.json, no wrapper key, type accepting prompt as well as command), Factory (.factory/hooks.json, also no wrapper key, nine events, type always "command"), Kiro (.kiro/hooks/<name>.json, a {"version": "v1", "hooks": [...]} array), Qoder (.qoder/settings.json hooks, merged alongside mcpServers in the same file), OpenHands (.openhands/hooks.json, six events), Augment (.augment/settings.json hooks, merged alongside mcpServers in one write; five events; timeout in milliseconds, converted from this field's seconds; command must be a path ending in .sh/.ps1/.cmd/.bat, or it never runs), Crush (crush.json hooks, PreToolUse only, merged alongside mcp in the same file), Trae (.trae/hooks.json, the same integer {"version": 1, "hooks": {...}} wrapper around Claude-shaped {matcher, hooks: [...]} groups, six events, plus a loop_limit on Stop), Goose (.agents/plugins/agnostic-ai/hooks/hooks.json, with the required Open Plugins manifest beside it), and Copilot (.github/hooks/agnostic-ai.json, an integer {"version": 1, "hooks": {...}} wrapper, 14 events, command, HTTP, and sessionStart prompt handlers, timeoutSec; both PreToolUse and Copilot's own camelCase preToolUse are independently valid event-key spellings in that same file, so this one also passes event: through verbatim rather than picking one). Zed is opt-in: ordinary hooks become manual tasks, while WorktreeCreate maps to a task with hooks: ["create_worktree"]. Without outputs.zed.tasks-file, Zed raises a coverage note. 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, BeforeAgent, AfterAgent, Notification, SessionStart, SessionEnd, PreCompress, BeforeModel, AfterModel, BeforeToolSelection), so a Gemini hook must set event: to one of those. agnostic-ai validate flags any event a target does not recognize.

Per-target rendering

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: run_shell_command
command: echo "tests please"

Renders to .gemini/settings.json (nested command handlers, event name passed through unchanged):

{
  "hooks": {
    "AfterTool": [
      {"matcher": "run_shell_command", "hooks": [{"type": "command", "command": "echo \"tests please\""}]}
    ]
  }
}

When command is a list, each entry becomes a separate handler (Claude, Codex, Gemini). Gemini keeps the handlers in one definition. Set x-gemini.sequential: true to run them in order. description reaches each handler; x-gemini.name sets its native display name, and x-gemini.env supplies per-handler environment variables.

Gemini hook imports preserve nested definitions and accept old flat files. A single handler imports with a timeout in seconds when exactly representable as a whole second; otherwise x-gemini.timeout retains the native milliseconds. A group with multiple handlers uses x-gemini.hooks, a native handler array that preserves each command, name, description, environment map, and millisecond timeout. This array replaces command emission for Gemini.

MCP servers

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

The name is the server identifier, not the source filename. It can use package-style forward slashes, such as npm:@modelcontextprotocol/server-sequential.thinking. agnostic-ai percent-encodes slash-bearing names when it needs one YAML file per server, while the YAML name and every generated target config keep the original value. Other spec kinds still require one safe path segment because their names become output paths.

FieldRequiredDefaultDescription
nameyesnoneServer identifier. Becomes the key in the generated config.
descriptionnoemptyFree-form documentation.
typenostdioTransport: stdio, http, sse, or ws. Remote transports (http, sse, ws) emit an explicit type; stdio stays type-less since it is the inferred default.
commandstdio onlynoneExecutable to launch.
argsnoemptyArgument list for the command.
envnoemptyEnvironment variables passed to the server.
cwdnoemptyWorking directory for the stdio server process. Codex, Gemini, OpenCode, Qoder, Continue, Copilot/VS Code. Warp maps this to its own working_directory field.
env_varsnoemptyExtra environment variables allowed for a Codex stdio server. Entries are names or {name, source} objects, where source is local or remote.
urlhttp/sse/ws onlynoneEndpoint URL.
headersnoemptyHTTP headers for http/sse transports.
env_http_headersnoemptyCodex HTTP headers mapped to the environment variable that supplies each value.
envFilestdio only, Cursor + Copilot/VS CodeemptyPath to an env file loading additional variables (e.g. .env, ${workspaceFolder}/.env). Not supported on a url (remote) entry.
devno, Copilot/VS Codeemptywatch is a glob or glob array that restarts stdio, HTTP, or SSE servers on change. debug: {type: "node"|"debugpy", debugpyPath} is stdio-only. Remote debugging produces a coverage note; watch patterns still emit. Neither field reaches Copilot CLI.
sandboxEnabledstdio only, Copilot/VS CodefalseRun the server in a sandboxed environment. macOS and Linux only.
authnoemptyTwo unrelated shapes by target. Codex HTTP authentication fallback, a string: oauth or chatgpt. Cursor static OAuth on a remote (url) entry, an object: {CLIENT_ID, CLIENT_SECRET, scopes} (CLIENT_ID required, the other two optional).
http_headers_helperhttp only, CodexemptyLocal command that prints a JSON object of HTTP header names/values, for a locally connected HTTP MCP server.
requiredno, CodexfalseFail startup/resume if this enabled MCP server cannot initialize.
startup_timeout_secno, Codex10Override the server's startup timeout, in seconds. Distinct from timeout below, which is milliseconds on the targets that use it; Codex's own field name says sec so the two never conflate.
startup_timeout_msno, Codex10000The same startup timeout in milliseconds. Codex documents it as an alias, so set one or the other, not both.
tool_timeout_secno, Codex60Override the per-tool execution timeout, in seconds. Same unit note as startup_timeout_sec.
default_tools_approval_modeno, CodexunsetDefault approval behavior (auto, prompt, writes, or approve) for this server's tools, unless a per-tool override exists.
scopeshttp/sse only, CodexemptyOAuth scopes to request when authenticating to this MCP server.
oauth_resourcehttp/sse only, CodexemptyRFC 8707 OAuth resource parameter to include during MCP login.
experimental_environmentno, Codexunsetlocal or remote placement for the server. remote starts a stdio server through a remote executor environment; HTTP remote placement is documented as not yet implemented.
enabled_toolsno, Codex + CrushemptyAllow list of tool names exposed by the server.
disabled_toolsno, Codex + CrushemptyDeny list applied after enabled_tools.
sessionlessno, CrushfalseMark a server that sends no Mcp-Session-Id so Crush skips the subscriptions/listen stream it would otherwise reject. Leave unset to let Crush auto-detect known sessionless servers such as GitHub MCP.
trustno, Gemini + QoderfalseBypass all tool-call confirmations for this server.
includeToolsno, Gemini + QoderemptyAllowlist of tool names exposed from this server. On Amp the same concept exists but goes in namespaced as x-amp.includeTools: Amp's MCP page enumerates its fields without naming it, so a top-level mapping would assert more than the vendor states.
excludeToolsno, Gemini + QoderemptyDenylist of tool names; takes precedence over includeTools on a name in both.
alwaysAllowno, QoderemptyTool names always allowed without confirmation.
autoApproveno, KiroemptyTool names to auto-approve without prompting. "*" auto-approves all of the server's tools.
disabledToolsno, Kiro + FactoryemptyTool names to omit when calling the agent.
alwaysLoadno, Claude CodefalseLoad every tool from this server into context at session start instead of deferring it behind tool search. Available on all transports.
headersHelperhttp/sse/ws only, Claude CodeemptyCommand run at connection time that prints headers to merge into the connection, for a server on Kerberos, short-lived tokens, or internal SSO.
oauthScopeshttp/sse only, KiroemptyOAuth scopes to request. Overridden by oauth.oauthScopes when both are set; an explicitly empty list emits as written, since Kiro documents [] as the remedy for scope errors.
oauthnoemptyTarget-specific shapes, each mapped to the keys its own vendor documents. Claude Code (http/sse): {clientId, callbackPort, authServerMetadataUrl, scopes}, where scopes is one space-separated string; clientSecret is never written, since Claude Code keeps it in the system keychain. Kiro (http/sse): {clientId, clientSecret, redirectUri, clientMetadataUrl, oauthScopes}. Qoder: passed through as declared, since the vendor's own field list is open-ended. Crush: a plain boolean toggle, paired with the separate oauth_client_id / oauth_client_secret / oauth_callback_port fields. Copilot/VS Code (http/sse): {clientId, enterpriseManaged}. Codex (http/sse): {client_id, callback_url, callback_port}, a nested [mcp_servers.<id>.oauth] table rather than a top-level object.
connectionTimeoutno, ContinueemptyMCP connection timeout in milliseconds. Preserved on stdio and remote servers.
requestOptionsremote, ContinueemptyNative HTTP options, including timeout, verifySsl, caBundlePath, proxy, clientCertificate, and headers. Portable headers fill this map; an explicitly supplied native header wins a duplicate key.
connectTimeoutno, FactoryemptyMCP connection timeout in milliseconds, including explicit zero.
api_keynoemptyOpenHands credential for an http/sse server. Upgrades the emitted sse_servers/shttp_servers element from a bare URL string to { url, api_key }, OpenHands' own documented object form. headers has no equivalent there and surfaces a coverage note instead.
timeoutnoemptyTwo unrelated units by target. OpenHands: tool-execution timeout in seconds (1-3600, default 60) for an http server; documented for the SHTTP tab only, so it upgrades shttp_servers elements the same way api_key does, and an sse entry that sets it surfaces a coverage note instead. Gemini, Claude Code, OpenCode, Qoder, Factory, and Kilo: milliseconds, any transport. Claude Code's is a per-tool-call execution timeout, OpenCode's a tool-fetch timeout defaulting to 5000.
disablednofalseSupport varies by target; see disabled support by target below.
rootsnoemptyList of {uri, name} objects. Passed to targets that support MCP roots (Claude Code, Cursor, Copilot).

Factory HTTP/SSE servers accept oauth: false or an object containing scopes, resource, authorizationServerIssuer, clientId, clientSecret, clientMetadataUrl, tokenEndpointAuthMethod, and callbackPort. Kilo remote servers accept oauth: false; OAuth objects are not emitted there. Factory and Kilo preserve explicit zero timeouts. Use x-factory, x-kilo, or x-continue to override the corresponding top-level options for that target.

command and url are the two fields a server cannot work without, and agnostic-ai lint reports a missing one as an error (LINT008). Neither validate nor sync catches it: some targets drop the entry, the rest write a server object with no way to start or reach anything, and both do it silently. See lint.

Targets with native MCP propagation:

TargetFileSchema
Claude Code.mcp.jsonstandard mcpServers
Cursor.cursor/mcp.jsonstandard mcpServers
Copilot / VS Code.vscode/mcp.json + .github/mcp.jsonservers with type field for VS Code; mcpServers for Copilot CLI, which does not read the VS Code file
Codex.codex/config.toml[mcp_servers.<name>] table
Gemini.gemini/settings.jsonmcpServers (uses httpUrl for HTTP)
Continue.continue/mcpServers/<name>.yamlone YAML per server
Amp.amp/settings.jsonamp.mcpServers (dotted key)
Zed.zed/settings.jsoncontext_servers (stdio: command/args/env; HTTP/SSE: native url/headers)
Warp.warp/.mcp.jsonstandard mcpServers (stdio cwd maps to working_directory)
OpenCodeopencode.jsonmcp with type: local|remote
Antigravity.agents/mcp_config.jsonmcpServers (remote uses serverUrl, not url)
Factory.factory/mcp.jsonstandard mcpServers
Qoder.qoder/settings.jsonstandard mcpServers, merged so unrelated settings keys survive
OpenHandsconfig.toml[mcp] table with stdio_servers / sse_servers / shttp_servers arrays, no type field; a remote entry is a bare URL string, or { url, api_key, timeout } once api_key and/or (shttp only) timeout is set
Trae.trae/mcp.jsonstandard mcpServers, no type field (stdio: command/args/env; HTTP: url/headers)
Windsurf.devin/mcp_config.jsonmcpServers (Devin Local's file, not Cascade's; remote uses transport, not type)
Augment.augment/settings.jsonstandard mcpServers, merged into the file alongside shell, theme, and other Auggie CLI settings rather than overwriting it

Aider, Cline, Jules, and Goose have no project-scoped MCP file and skip with a warning.

disabled support by target

Confirmed per target, never generalized: a target not listed here has not been checked.

TargetBehavior
AntigravityNative disabled boolean in .agents/mcp_config.json (default false); passes through unchanged under that literal name, unlike Codex and Kilo Code which map it to enabled: false.
CodexMaps to enabled = false in .codex/config.toml.
CrushNative disabled boolean in crush.json (default false); passes through unchanged. Confirmed in the vendor's published schema.json ($defs.MCPConfig.properties.disabled), not in the README.
FactoryNative disabled boolean in .factory/mcp.json (default false); passes through unchanged.
Kilo CodeMaps to "enabled": false in kilo.jsonc; an enabled server gets no key at all.
KiroNative disabled boolean in .kiro/settings/mcp.json (default false); passes through unchanged on both local and remote servers.
OpenCodeMaps to "enabled": false in opencode.json; an enabled server gets no key at all. import opencode reads it back into disabled: true.
QoderNative disabled boolean in .qoder/settings.json (default false); passes through unchanged. It had no effect while qoder shared Claude Code's .mcp.json, and works now that qoder writes its own file.
WindsurfNative disabled boolean in .devin/mcp_config.json (default false); passes through unchanged. devin mcp enable/disable toggle the same key.
ZedMaps to "enabled": false in .zed/settings.json; an enabled server gets no key at all, since Zed defaults it to true. Confirmed in Zed's own settings struct (crates/settings_content/src/project.rs), not on its MCP doc page, which names no per-server toggle.
Claude Code, Cursor, Copilot, Trae, AugmentNo file-based way to pre-disable a project-scoped MCP server; the field has no effect and agnostic-ai does not emit it. Disable the server from the target's own UI instead (Augment: auggie mcp remove).
WarpNo file-based way to pre-disable a project-scoped MCP server: Warp's two property tables are closed lists and neither carries a disable key. Little is lost, since "project-scoped servers never auto-spawn" there; start the server from the MCP servers page when you want it.

tools support by target

Confirmed per target, never generalized: a target not listed here has not been checked. One tools: [Read, Bash] spec produces five different outcomes, so read this before assuming an allowlist restricts anything.

TargetBehavior
Claude Code, CopilotPasses through verbatim as a YAML list. The vendor vocabulary matches agnostic-ai's Claude-style names.
JuniePasses through verbatim as a YAML list, at the agent's native .junie/agents/<name>.md file (#604). Junie's own built-in tool group labels (Read, Bash, Glob, Grep, Write, Edit, WebSearch, plus AskUserQuestion, which has no Claude equivalent) match agnostic-ai's Claude-style names for every group Junie documents, though it documents fewer groups overall (no WebFetch, Task, TodoWrite, or NotebookEdit). disallowedTools (a denylist applied after tools) passes through the same way.
QoderPasses through as a comma-separated string (tools: Read, Bash), the only form Qoder documents. Its built-in vocabulary is Claude-style, so the names carry over unchanged.
TraePasses through as a comma-separated string (tools: Read, Bash), the form Trae's subagent doc documents, at the agent's native .trae/agents/<name>.md file (#638). Its vocabulary is Claude-style and covers agnostic-ai's set exactly, plus Skill, LSP, TodoWrite, and mcp__<server>__<tool>. x-trae.disallowedTools (a denylist that wins over tools) reaches the file the same way.
WindsurfTranslated, not passed through, and written under Devin's own key name allowed-tools. Devin publishes a complete five-name vocabulary (read, edit, grep, glob, exec), so Read/Grep/Glob/Bash map one-to-one and Write/Edit both collapse onto edit, which means an agent declaring only Write also gains edit capability. An mcp__<server>__<tool> name passes through untranslated. Names outside the table surface a coverage note rather than being dropped silently. x-windsurf.allowed-tools bypasses translation entirely.
AntigravityNo effect. Antigravity's names (view_file, replace_file_content, grep_search, run_command, ...) share nothing with agnostic-ai's, and the vendor warns that "Specifying an unmapped or misspelled tool name in the tools list may cause the subagent process to hang during execution", so a verbatim passthrough is worse than dropping. Set x-antigravity.tools. A coverage note fires.
OpenHandsNo effect. OpenHands agents use native names such as file_editor and terminal, so the generic Claude-style list is dropped with a coverage note. Set x-openhands.tools and use a distinct outputs.openhands.agents-dir when the resulting file must differ from Goose's shared default.
GooseNo effect. Goose's documented project-agent fields are name, description, and model; it documents no tools field. A coverage note fires.
KiroTranslated, not passed through. Kiro's vocabulary is lowercase categories, so Read/Grep/Glob become read, Write/Edit become write, Bash becomes shell, and WebFetch/WebSearch become web. A category grants more than the single name it came from: Edit alone also permits delete_file. Names outside the table surface a coverage note rather than being dropped. x-kiro.tools bypasses translation entirely.
FactoryTranslated, not passed through, onto a different vocabulary than Kiro's. Droid CLI's complete tool-ID table is Read, LS, Grep, Glob, Create, Edit, ApplyPatch, Execute, WebSearch, FetchUrl, and "Unknown IDs cause a validation error", so Bash becomes Execute, Write becomes Create, and WebFetch becomes FetchUrl; every other agnostic-ai name is already a valid ID and carries over. TodoWrite and Skill drop because Factory always grants them, and ExitSpecMode and GenerateDroid drop because listing either one is a validation error. Any other name drops with a coverage note instead of failing the whole droid at load time. x-factory.tools bypasses translation, the only way to reach a category name (read-only) or a registered MCP tool ID.
CodexNo effect. Codex uses tools as a configuration table rather than a Claude-style allowlist, so the field is not written and a coverage note fires. Set x-codex.tools for Codex-native settings.
CursorNo effect. Cursor subagents document only name, description, model, readonly, and is_background. readonly: true is the coarse equivalent. A coverage note fires.
AugmentNo effect. Augment's names (view, codebase-retrieval, str-replace-editor, ...) are its own vocabulary, and no vendor-published mapping exists. Set x-augment.tools / x-augment.disabled_tools. A coverage note fires.
Kilo CodeNo effect. Kilo Code's agent schema has no tools key at all; access is controlled by a permission object. Set x-kilo.permission. A coverage note fires.

Every target that cannot honor the field says so at sync time. A silently dropped restriction is the failure this table and those notes exist to prevent: an author who writes tools: [Read] and gets an unrestricted agent has no way to notice.

color support by target

Confirmed per target, never generalized: a target not listed here has not been checked. color is a shared top-level key on three targets, and each documents its own value space, so a value valid on one may go unrecognized on another.

TargetDocumented values
AugmentFree text: "should be a valid ANSI color name" (docs.augmentcode.com/cli/subagents).
Kilo CodeHex (#FF5733) or a theme token (primary, accent, error, and others the doc leaves open-ended with "etc.") (Kilo-Org/kilocode custom-subagents.md).
QoderOne of eight named values: red, blue, green, yellow, purple, orange, pink, cyan (docs.qoder.com/cli/subagent).

agnostic-ai writes color verbatim to whichever of these three targets an agent spec reaches; it does not validate the value against any target's vocabulary. color: blue is a valid ANSI name for Augment and one of Qoder's eight named values, but is neither hex nor a listed Kilo Code theme token, so it may not render there as intended (Kilo Code's "etc." leaves other tokens possible, not confirmed). An unrecognized value is cosmetic: the agent still runs, only the badge may not show the intended color.

Commands

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.
FieldRequiredDefaultDescription
namenofilenameCommand identifier. Becomes the slash name (e.g. /deploy).
descriptionnoemptyOne-liner shown in slash-command pickers.
argument-hintnoemptyHint string shown after the command. Claude, Augment, and Factory.

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), Cursor (.cursor/commands/<name>.md), Gemini (.gemini/commands/<name>.toml), OpenCode (.opencode/commands/<name>.md), Trae (.trae/commands/<name>.md), Junie (.junie/commands/<name>.md), Qoder (.qoder/commands/<name>.md), Kilo Code (.kilo/commands/<name>.md), Augment (.augment/commands/<scope>/<name>.md), and Factory (.factory/commands/<name>.md). Codex deprecated project prompts, so its commands emit only when outputs.codex.commands-dir is set; otherwise sync prints a coverage note. Amp has no file-based command surface at all (commands register programmatically via amp.registerCommand(...)), so, like other targets outside this list, it logs a warning and skips.

Settings

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
FieldRequiredDefaultDescription
permissions.allownoemptyPermission rules auto-approved without prompting.
permissions.denynoemptyPermission rules always blocked.
permissions.asknoemptyPermission rules that prompt before running.
modelnoemptyDefault 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), Codex (.codex/config.toml, model), Copilot (.github/copilot/settings.json, model), Qoder (.qoder/settings.json, permissions + nested model.name), OpenCode (opencode.json, model), Junie (.junie/config.json, model), and Kilo Code (kilo.jsonc, model). Unrepresentable settings fields produce the normal coverage summary while supported fields still emit. Import for Copilot, OpenCode, Junie, Qoder, and Kilo creates settings/imported.yaml from portable fields and leaves target-only native keys in place.

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.

import claude keeps permissions and model in the Claude-only overlay (.agnostic-ai/overlays/claude.settings.json), since a Claude model id does not necessarily port to other tools. Codex does the same through its config overlay. Imports from Copilot, OpenCode, Junie, Qoder, and Kilo write their portable fields to settings/imported.yaml, ready for cross-target sync. Review imported model identifiers before enabling more targets because vendors use different naming schemes. A settings spec's permissions still union with the imported Claude overlay on sync.

Reviews

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 uses .cursor/BUGBOT.md at the root and <scope>/.cursor/BUGBOT.md for scoped specs; outputs.cursor.review-file overrides the basename. Goose uses .agents/REVIEW.md and <scope>/.agents/REVIEW.md; outputs.goose.review-file overrides that scope-relative path. goose review combines instructions from changed-file directories and their ancestors. Both emit plain bodies without spec frontmatter. Other adapters report reviews as unsupported.

Environments

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-ai

Native 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.

Also native on OpenHands: the install field writes .openhands/setup.sh, the vendor's documented repository bootstrap script that "will run every time OpenHands begins working with your repository". The script body is install verbatim under a #!/bin/bash shebang. terminals has no OpenHands equivalent (the script runs once, synchronously; there is no long-running process surface) and surfaces a coverage note instead. Multiple environment specs merge the same way as Cursor's (last install wins). Override the path with outputs.openhands.setup-file.

Also native on Amp orbs: install writes an executable .agents/setup script, while each named terminal becomes a supervised service in .amp/services.yaml. Terminal fields after name pass through to the service, including Amp-specific fields supplied through x-amp.terminals. Service names must match Amp's lowercase, number, and hyphen rules, and each service requires command. Multiple environment specs merge by top-level key, last value wins. Override the paths with outputs.amp.setup-file and outputs.amp.environment-file. Amp's .agents/resume is not emitted: it runs after activation and every wake with thread credentials, which is distinct from dependency installation and supervised services.

Other targets (devcontainers, Codex setup scripts) have no emitter yet and report the spec as unsupported.

Ignore

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 .geminiignore, Aider .aiderignore, Windsurf .devinignore, Kiro .kiroignore, Trae .trae/.ignore, Junie .aiignore, Crush .crushignore, and Kilo .kilocodeignore (a compatibility input migrated into read/edit permission denials). Each spec keeps its pattern order and whitespace. Outer line breaks are trimmed, CRLF becomes LF, 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.

Overwrite behaviour

Sync replaces an ignore file without an agnostic-ai provenance header only when it can establish that existing exclusions survive. Every pattern must remain unchanged and in the same order. Extra exclusion patterns are allowed. Missing or reordered patterns, added negations (!pattern), and changed whitespace fail with AAI-103 and leave the file untouched. The comparison is conservative: equivalent spellings or harmless reorderings can still fail.

.kiroignore: hand-authored ignore file cannot be safely overwritten: existing
patterns are missing or reordered: "my-secrets/", "*.key". Run
`agnostic-ai import kiro` to copy its patterns into an ignore spec, then keep
their order and review any added negations before syncing again.

agnostic-ai import <target> reads the file into ignore/<target>.md, preserving comments, pattern order, and whitespace. Import removes a leading UTF-8 byte-order mark so the generated header does not turn it into a pattern character. Syncing unchanged imported patterns needs no manual cleanup. If other specs add negations or repeat the imported patterns in a different order, review the combined order before syncing. Already-generated files still regenerate from their specs, including intentional pattern removals.

Comment and blank lines exclude nothing, so a file holding only those never blocks a sync. A hash is a comment prefix only at the start of a line; leading spaces and tabs can be part of a pattern. outputs.<target>.provenance-header: false removes the marker used to recognize generated output and disables this check. Dry-run skips it because no file is written; sync --check still reports unsafe overwrites.

Frontmatter rules

  • 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.

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, from one source file.

Put new skills in {{$SKILLS_DIR}} and agent profiles in {{$AGENTS_DIR}}.

Five variables are available:

VariableResolves 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

Rules:

  • Bodies only. Every spec kind that carries a body is expanded; frontmatter values are not.
  • An outputs.<target>.<field> override wins. Set outputs.claude.skills-dir: custom/skills and {{$SKILLS_DIR}} follows it, so a body never names a directory the emitted tree does not use.
  • A variable the target has no surface for stays verbatim and raises a coverage note. It is not blanked, because turning "see {{$COMMANDS_DIR}}" into "see " loses the sentence silently. Targets that carry every kind in one entry-point document (aider, jules) resolve no variables at all.
  • A variable is declared only where the target has a dedicated surface for that kind. Several targets flatten agents into their rules directory with a filename prefix (continue, trae, windsurf) or render them as commands (gemini); those declare no {{$AGENTS_DIR}} rather than point at a directory that is not an agents directory. Antigravity, Goose, and OpenHands resolve it to .agents/agents.
  • The $ sigil is required. Plain {{placeholder}} is left alone, so Warp workflow arguments and any Handlebars or Jinja quoted in prose survive untouched. Lowercase names ({{$skills_dir}}) do not resolve.

Target-specific extensions: x-<target> namespace

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.
TargetResulting frontmatter
claudename, description, model, allowed-tools
cursorname, description, model, globs, alwaysApply
geminidescription (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 .agents/skills/<name>/agents/openai.yaml for UI customization, policy, and tool dependencies.

Arbitrary custom keys

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:

TargetSurfaceCustom key lands in
claudeSKILL.md frontmatterevery x-claude key (e.g. disable-model-invocation: true)
codexSKILL.md frontmatterevery x-codex key except interface/policy/dependencies (those route to openai.yaml)
amp, zed, crush, gemini, opencode, copilot, kiroSKILL.md frontmatter (shared renderer)every x-<target> key beyond name/description (e.g. crush's user-invocable: true, which adds the skill to the command palette)
cursorSKILL.md frontmatterevery x-cursor key beyond name/description/paths/disable-model-invocation/icon/color/metadata
cursoragent .md frontmatterevery x-cursor key beyond name/description/model/readonly/is_background
copilotrule .instructions.md frontmatterevery x-copilot key, alongside applyTo
copilot.agent.md frontmatterevery x-copilot key beyond name/description/tools/model
opencodeagent .md frontmatterevery x-opencode key beyond description/mode/model/temperature/permission
opencodecommand .md frontmatterevery x-opencode key beyond description/agent/model/subtask
geminicommand .tomlevery x-gemini key (string, bool, number, or string array)
geminiagent .md frontmatterevery x-gemini key beyond name/description/kind/model/temperature/max_turns/timeout_mins (e.g. mcpServers, Gemini's inline per-agent MCP servers; x-gemini.tools also wins outright over the translated tools list)
kiroagent .md frontmatterevery x-kiro key beyond name/description/model; name is handled separately as Kiro's display name while the filename remains the canonical spec identity
kirohook .json entryevery x-kiro key beyond name/trigger/matcher/action/timeout/enabled/description (e.g. confirm, Kiro's Stop-hook confirmation block, which has no agnostic-ai spec equivalent)
qoderagent .md frontmatterevery x-qoder key beyond name/description/model/tools/skills/mcpServers
warpworkflow .yamlevery x-warp key beyond name/command/description/tags (e.g. shells, arguments, source_url, author, author_url)
zedtask (in outputs.zed.tasks-file)every x-zed key beyond label/command/args/hooks (e.g. cwd, env, shell, reveal, hide, save, allow_concurrent_runs, use_new_terminal, tags, reevaluate_context); hooks is handled separately so WorktreeCreate can add create_worktree without dropping other native hook names

Targets that emit no surface for a spec kind drop arbitrary custom keys (kiro rule steering files carry no passthrough; kiro agents, skills, and hooks do, see above). Gemini TOML accepts scalars and string arrays only; nested tables are skipped.