Agents
agents/ defines subagents that handle tasks for the main agent. Each has its own instructions, tools, and model. For example, a reviewer can have read-only access, while a test writer uses a cheaper model.
Give the agent one responsibility. Name the files or decisions it owns, the result it should return, and how to check that result.
Use description to say when to delegate to it ("use proactively" makes Claude Code delegate more often). Keep detailed instructions in the body, which loads only when the agent runs. Descriptions load in every session.
When not to write one
- Keep work in the main conversation when it needs frequent back-and-forth, or when planning, implementation, and testing share one context. An agent starts fresh and returns a summary.
- Keep a quick change, or a task where speed matters, in the main conversation. An agent may need time to gather context first.
Write one
agnostic-ai new agent code-reviewer
Edit .agnostic-ai/agents/code-reviewer.md (or its configured source path). The YAML frontmatter configures the agent; the body is its prompt.
---
name: code-reviewer
description: Reviews diffs for bugs, style, and architectural issues. Use after a change set is complete.
can: [read, shell(git diff *)]
model: {claude: sonnet, codex: sol}
---
You are a code reviewer. Report concise findings with `file:line` references.
Claude Code gets tools: [Read, Bash] and can run any command, because it ignores the pattern (capabilities). Codex drops can with a note (tool support).
Run agnostic-ai explain agents/code-reviewer.md to inspect the translations, then agnostic-ai sync and agnostic-ai sync --check.
Fields
| Field | Required | Default | Description |
|---|---|---|---|
name | no | filename without .md | Agent identifier and output filename. |
description | no | empty; the name for Claude Code, Qoder, Antigravity, and Gemini CLI | When to hand a task to the agent. Claude Code skips an agent without one, so sync writes the name there with a note. lint warns when it is missing (LINT048). |
can | no | unset | What the agent may do, in tool-neutral names. See capabilities. |
tools | no | unset | The same list in Claude Code tool names. Set can or tools, not both. See support by target. |
model | no | unset | A string, a map per target, or a tier name. See per-target model and effort. |
effort | no | unset | A string or integer, or a map per target. Same section. |
color | no | unset | Badge color. See color support by target. |
readonly | no | unset | true limits the agent to reading; false does nothing. See readonly by target. |
memory | no | unset | Persistent memory directory scope: user, project, or local. Claude Code uses it, Qoder gets the key (unconfirmed), and Junie gets it as written. Every other tool drops it. |
mcpServers | no | unset | MCP servers this agent may reach. See support by target. |
maxTurns | no | unset | The most turns before the agent stops, a whole number of 1 or more. See support by target. |
background | no | unset | true runs the agent without making the main session wait. Same section. |
permissionMode | no | unset | How much this agent may do without approval. See support by target. |
hooks | no | unset | Lifecycle hooks for this agent only. Same section. |
Any other frontmatter field is written as given.
readonly by target
| Target | Effect |
|---|---|
| Cursor | Restricted |
| Claude Code | disallowedTools: Write, Edit, NotebookEdit (Bash stays allowed) |
| Codex | sandbox_mode = "read-only", which current Codex ignores (the agent keeps the session sandbox), and a sync note |
| Qwen Code | disallowedTools: [write_file, edit, notebook_edit] (shell stays allowed) |
| Factory | tools: read-only with mcpServers: [] unless servers are listed. Wins over a portable tools list |
| Other targets | Sync note |
An explicit x-claude.disallowedTools, x-qwen.disallowedTools, x-codex.sandbox_mode, or x-factory.tools wins.
Capabilities
can lists what an agent may do, in names that belong to no one tool:
can: [read(src/**), edit(src/**), shell(go test *), mcp:github]
can value | Grants | Claude Code name |
|---|---|---|
read, read(<path>) | Read files, or matching files | Read; the path is dropped |
write | Create or overwrite files | Write |
edit, edit(<path>) | Edit files, or matching files | Edit; the path is dropped |
delete | Delete files | No native tool |
shell | Run any command | Bash |
shell(<pattern>) | Run the commands that match the pattern | Bash; the pattern is dropped |
web | Fetch pages and search the web | WebFetch, WebSearch |
mcp:<server> | Use every tool of one MCP server | mcp__<server> |
mcp:<server>/<tool> | Use one MCP tool | mcp__<server>__<tool> |
- Each capability becomes the tool's own name.
deleteraises a sync note on tools with no delete tool. The names match the hookmatch:tool kinds. - Claude Code names work as aliases, and a list can mix both:
can: [read, Grep].Agent(<type>)andx-claude.toolsare written as given. - Claude Code reads an agent's
toolsby name only, so sync drops a path or pattern there and prints a note such asclaude: .agnostic-ai/agents/code-reviewer.md: shell(git diff *) becomes Bash; Claude Code does not limit an agent's tools to a pattern.on-unsupported: errorfails on it. To limit commands there, add aPreToolUsehook to the agent'shooks, or deny commands in a settings spec's permissions, which apply to every agent. A skill'sallowed-toolskeeps the pattern. - Claude Code refuses to launch an agent with an empty tool list, such as
can: []orcan: [delete]. Sync leaves it out of.claude/agents/with a note. - For Claude Code, a server name that contains
__or ends in_, such asreports__v2, becomesmcp__<server>__*, the only whole-server form Claude Code matches for it. validate,lint(LINT036), andsyncstop on an unknown capability, oncanbesidetools, and oncanunderx-<target>.- A capability is only as strong as the tool's own permission system. agnostic-ai adds no sandbox.
agnostic-ai migrate --only capabilities rewrites tools as can; names with no capability of their own stay as aliases. import writes capabilities when each tool name matches one. lint --suggest-capabilities suggests neutral names for aliases (LINT037), off by default.
Per-target model and effort
model: and effort: each take a single value or a map keyed by target name, with an optional default. The first match wins:
x-<target>.<key>(nulldeletes the key)<key>.<target><key>.default- Nothing is written, and the tool uses its own default.
A target value that is not a single value falls back to default.
---
name: architect
description: Designs the change before anyone codes it.
model:
claude: opus
cursor: "claude-opus-5[effort=high]"
default: gpt-6.1-sol
effort:
claude: xhigh
qoder: 8000
default: high
x-codex:
model_reasoning_effort: xhigh
---
Claude gets opus and xhigh. Qoder gets gpt-6.1-sol and 8000. Cursor gets its model id and no effort. Codex gets gpt-6.1-sol with effort xhigh from x-codex.
effort values by target. Without effort, the agent uses the session's level.
| Target | Values | Output |
|---|---|---|
| Claude Code | low, medium, high, xhigh, max, model dependent | effort, not checked |
| Qoder | Same five names, or a positive integer | effort |
| Junie | Alias of reasoningLevel | effort |
| Factory | low, medium, high | reasoningEffort; other values raise a sync note |
| Codex | Any string | model_reasoning_effort; an integer raises a note |
| Cursor | none | Sync note. Put it in the model id, as above |
| Copilot | none | Sync note. x-copilot.effort still passes through |
| Augment | off, minimal, low, medium, high, xhigh | thinking (Auggie 1.x); other values raise a sync note, and x-augment.thinking wins |
| Every other target | none | Sync note |
Claude model names on other targets. A shared model (a single value or default) set to a Claude model name is left out, with a sync note, on a tool that cannot load it, so that tool uses its own default. The note suggests model: {claude: <name>}, or names the tier to fix when the name sits in a tier's default. on-unsupported: error fails the sync instead. A value under model.<target> or x-<target>.model is written as given. import claude writes these names as model: {claude: <name>}, and import codex adds a Codex agent model as model.codex.
| Target | Claude names that raise the note |
|---|---|
| Codex, Gemini, OpenCode, Kilo Code | The aliases, inherit, and claude-* ids |
| Cursor, Factory, Kiro | The aliases |
Claude Code's aliases are sonnet, opus, haiku, fable, best, opusplan, sonnet[1m], and opus[1m]. The value default resets Claude's model instead of naming one, so it raises no note.
Model tiers
A tier names a model role once in agnostic-ai.yaml, such as strong: {claude: opus, codex: sol, effort: high}. Write model: strong in an agent, and each target the tier names gets its model; the others keep their default. A tier's description tells the main agent when to pick it. For model, the first match wins:
x-<target>.modelmodel.<target>in the spec- The tier's entry for the target
- The tier's
default - The tool default
To override one target, write the tier as the map's default: model: {codex: gpt-6-luna, default: strong}.
- The tier's
effortapplies only when the spec sets noeffort, and is skipped for a target whose model the spec sets itself. - A value under
model.<target>orx-<target>.modelis never a tier name.model.<target>can be a vendor alias such ascodex: sol. explain agents/<name>.mdlists the model and effort each configured tool gets.modelslists the tierlintchecks (LINT025, LINT026).import claudesuggests a tier when two or more agents set the same Claude model.import claudeandimport codexkeepmodel: strongwhen the imported model and effort match what the tier gives that tool.
Agents as skills
Sync drops agent specs for Amp, Crush, Warp, and Zed by default. Set outputs.<target>.agents: skill to write each agent as a skill, which loads only when the model needs it.
outputs:
amp:
agents: skill
- The skill is
<skills-dir>/<agent>/SKILL.md, with the agent'snameanddescription. Its body starts with a note: the model plays the role in the main session and says so. Nothing enforces that separation. - Fields a skill cannot carry, such as
toolsandmodel, are dropped with a note. - An agent reference in skills, agents, and commands becomes that tool's skill phrase. A rule in a shared entry-point file such as
AGENTS.mdkeeps the neutral phrase. - An agent and a skill with the same name would share a folder, so
validatefails on them. - These four tools write
.agents/skills/, which Codex, Copilot, Gemini, Cline, Cursor, OpenCode, Junie, and Qwen read too, and several other tools with subagents write there. When one of those is enabled, sync keeps the agents out of that directory and names the tool. Setoutputs.<target>.skills-dirto a separate directory to use this option beside them. - The key cannot be combined with
rules-fileor, on Warp,workflows-dir. Other targets reject the key.
can and tools support by target
A tool that cannot enforce the list prints a sync note. Check these notes before relying on can: [read] to restrict an agent.
| Target | Behavior |
|---|---|
| Claude Code, Copilot, Junie | Written as a YAML list |
| Qoder, Trae | Written as a comma-separated string (tools: Read, Bash) |
| Windsurf, Kiro, Factory, Gemini, Kilo Code, OpenCode | Changed to the tool's own names or a permission map |
| Antigravity, OpenHands, Goose, Codex, Cursor, Augment | Dropped with a note |
On Trae, an explicitly empty list disables all tools, and an unset field loads every tool. Use x-trae: {tools: ""} to restrict only Trae.
Translation can grant extra access: on Kiro, edit also allows delete_file. Sync prints a note naming the extra access, and on-unsupported: error fails. explain agents/<name>.md lists each tool's names and any widening. Most tools accept their own names through x-<target>.tools, which skips the change.
mcpServers support by target
mcpServers names the servers, usually from mcps/, that one agent may reach. Without it, the agent gets the session's full set. An agent-only server reaches only the agents that name it.
| Target | Behavior |
|---|---|
| Claude Code, Qoder | Server names in the agent file. An agent-only server is written in full instead |
| Kiro | Every named server written in full, under mcpServers |
| OpenHands | Every named server written in full, under mcp_servers. Not when Goose shares .agents/agents/: set outputs.openhands.agents-dir |
| Junie, Factory | Server names in the agent file. An agent-only server's name is left out, except on Junie when it is the only name |
| Antigravity | Dropped; its docs give no shape for an inline server. Set x-antigravity.mcpServers |
| Gemini, Copilot | Dropped. Set x-gemini.mcp_servers or x-copilot.mcp-servers |
| Every other target | Dropped, and agent-only servers left out, with a sync note |
An x-<target> key for the list, such as x-kiro.mcpServers, replaces what sync would write. On Kiro and OpenHands, a name with no MCP spec is dropped with a note.
An empty list keeps every server on Junie and excludes every server on Factory, so list the servers you want.
maxTurns and background by target
A native key under x-<target>, such as x-gemini.max_turns, x-kilo.steps, or x-cursor.is_background, wins. An x-<target> override with the wrong type raises a sync note, and the shared value is used. validate, lint (LINT050), and sync stop on a maxTurns that is not a whole number of 1 or more, or a background that is not true or false.
| Target | maxTurns | background |
|---|---|---|
| Claude Code | maxTurns | background |
| Qoder | maxTurns | background, when background subagent sessions are on |
| Qwen Code | maxTurns | background |
| Junie | maxTurns | Sync note |
| Gemini CLI | max_turns | Sync note |
| Kilo Code, OpenCode | steps | Sync note |
| Cursor | Sync note | is_background |
| Every other target | Sync note | Sync note |
import reads each native key back into maxTurns or background. When the spec already sets the value under x-<target>, import writes it back there and keeps the shared value.
permissionMode and agent hooks support by target
Without either field, the agent uses the session's setting. On Claude Code, a session in bypassPermissions, acceptEdits, or auto mode runs the agent in that mode and ignores its value, and an agent's own bypassPermissions never applies.
| Target | permissionMode | Agent hooks |
|---|---|---|
| Claude Code | Seven values; manual is an alias of default | Written to the agent file |
| Qoder | Six values. Any other value is reported | Seven events. A wider setting is reported |
| OpenHands | Different names (always_confirm, never_confirm, confirm_risky). Set x-openhands | Six snake_case events, command handlers only. Set x-openhands |
| Qwen Code | Not written, with a note: Qwen maps Claude's names to its own approval modes with different strictness. Set x-qwen.approvalMode | Written to the agent file; Stop becomes SubagentStop. Project agent hooks need a trusted folder |
| CodeBuddy | Five values; any other value stops sync | Written to the agent file. Running them needs the user's allowUntrustedFrontmatterHooks choice |
| Grok Build | Written to the agent file | Written to the agent file |
| Junie | Written to the agent file | Copied with the frontmatter, with a note: Junie documents no agent hook field |
| Every other target | Sync note | Sync note |
On Qoder, bypassPermissions becomes acceptEdits when security policy disables it, and an agent runs fewer hook events than the project does.
color support by target
color is written as given and not checked. An unrecognized value affects only the badge.
| Target | Values |
|---|---|
| Augment | Free text, an ANSI color name |
| Kilo Code | Hex or a theme token, so color: blue may not show as expected |
| Claude Code, Qoder | One of eight names: red, blue, green, yellow, purple, orange, pink, cyan |
| OpenHands | Dropped with a note, since it shares .agents/agents/ with Goose, which has no color. Set x-openhands.color to a Rich color name |