Skills
skills/ holds procedures the agent loads only when a task needs them: cut a release, write a migration, triage a bug report. The tool keeps each skill's description in view. It reads the body and any bundled files when the skill applies.
- Small standing context. A long procedure costs nothing until it is used. A rule, by contrast, loads every session.
- Files that travel with it. Scripts, templates, and fixtures sit next to
SKILL.mdand land beside it in every tool. - Invoked by the model or by name. The model picks a skill from its description; a person can also call it directly.
- One format across tools. Targets that read the
SKILL.mdlayout get the skill as written. Targets without a skill surface get a rule and a coverage note.
Use a rule for what applies to every task, and a skill for a procedure some tasks need.
Write one
agnostic-ai new skill release-notes scaffolds one. Two layouts:
- Flat:
skills/yaml-validator.md - Nested, for skills with attached files:
skills/yaml-validator/SKILL.mdnext toskills/yaml-validator/schema.yaml
---
name: yaml-validator
description: Validate YAML against a schema. Use when a YAML file changes or the user asks to check one.
---
# YAML Validator
1. Read target file
2. Parse YAML and compare against `schema.yaml`
3. Report violations as `path: message`
Write description as the trigger: what the skill does and when to use it. The model decides from that line alone.
This manual-only release skill runs on a stronger model in Claude Code and ships its own script:
skills/cut-release/
├── SKILL.md
└── scripts/bump-version.sh
---
name: cut-release
description: Cut a release. Run only when the user asks to release or tag.
disable-model-invocation: true
model: {claude: opus}
---
1. Run `make ci-local` and stop on any failure.
2. Run `scripts/bump-version.sh <version>` from this skill's folder.
3. Commit `chore(release): v<version>` and tag it.
Fields
| Field | Required | Default | Description |
|---|---|---|---|
name | no | dir or filename | Skill identifier and output directory. Some targets restrict the format. |
description | no | empty | One-liner the model uses to decide whether to invoke the skill. |
argument-hint | no | unset | Hint for the arguments to a Claude Code slash command. Other targets omit it unless set under their x-<target> block. |
disable-model-invocation | no | unset | true keeps the skill out of automatic invocation; a person can still call it. See support by target. |
model | no | unset | Claude Code model for the rest of the turn. Scalar, per-target map, or tier name; x-claude.model wins. |
effort | no | unset | Claude Code effort for the rest of the turn. Scalar or per-target map; x-claude.effort wins. |
allowed-tools | no | unset | Neutral capabilities the skill may use, such as [read, shell(git diff *)]. See allowed tools. |
license | no | unset | The Agent Skills license, kept in every target's SKILL.md. |
workspaces | no | empty | Project directories where Cursor also gets a copy, such as [apps/web]. Cursor loads skills only from the workspace it opens, so a session or SDK agent started in apps/web misses a root skill. The skill stays at the root for every tool. |
A skill's scope comes from its folder. skills/services/api/review/SKILL.md moves the skill under services/api/, where only sessions in that directory load it. scope: in the frontmatter has no effect, and lint warns about it (LINT018). import cursor writes workspaces when a root .cursor/skills/<name> links to a skill folder under a project directory.
Sync reports a coverage note for each skill field a target omits, including argument-hint, model, and effort.
- Fields kept in native frontmatter, an enabled command mirror, or a policy sidecar get no drop note.
agnostic-ai compare claude codexshows which skill fields each target keeps, translates, or drops.- Use
{claude: opus}to choose a model only for Claude. - Global sync uses the same renderers. Shared global directories omit target overrides.
Allowed tools
Write allowed-tools with the same capabilities as an agent's can:
allowed-tools: [read(src/**), edit(src/**), shell(git diff *), mcp:github]
The names are read, write, edit, delete, shell, web, mcp:<server>, and mcp:<server>/<tool>. read and edit take path patterns. shell takes a command pattern. Each target writes its own names where it supports this field. A target that drops a restriction or grants more access raises a coverage note. on-unsupported: error fails on widening.
Claude Code names stay valid aliases: Read, Write, Edit, Bash, WebFetch, WebSearch, and mcp__<server>__<tool>. Scoped aliases such as Read(src/**) and Bash(git diff *) stay valid too. A list can mix neutral names and aliases.
agnostic-ai migrate --only capabilities rewrites aliases that map exactly, including an adjacent WebFetch, WebSearch pair as web. import writes neutral names where the native names map one to one. Neither guesses a narrower restriction from a target's tool bundle. lint --suggest-capabilities suggests neutral names for aliases (LINT037), with no suggestions by default.
Bundled files and output
Only SKILL.md and flat skills/*.md parse as skills. Every other file in a nested skill directory is a bundled asset (scripts, templates, fixtures, extra *.md). Assets copy verbatim to the same relative path under each target's skills dir. Import and sync preserve executable bits.
Most targets write <dir>/<name>/SKILL.md with assets. Several share .agents/skills/, so identical bytes are written once. Targets with no skill surface flatten the skill to a skill-<name>.md rule. Assets cannot follow, so they raise a coverage note. Set outputs.<target>.emit-skills-as-commands: true to also emit a slash command. Each target page gives the exact directory.
A body can point at another skill with {{$SKILLS_DIR}}, which resolves to each target's own skills directory.
Claude Code body syntax
Claude Code expands some syntax in a skill body before the model reads it (skills docs):
!`command`lines and```!blocks run the command and insert its output.$ARGUMENTSbecomes the text typed after the skill name.$0,$1, ... and$ARGUMENTS[N]become one argument each.
No other target documents this syntax for skills, so each one reads it as plain text. Sync copies the body as written. It prints one note per target and shape, naming each line:
note: `!`command`` on 1 skill has no effect on codex (the command does not run at .agnostic-ai/skills/pr/SKILL.md:8; put the line in a ::target claude fence)
on-unsupported: error fails the sync instead, and silent hides the note. lint reports each line as LINT019. sync --global raises the same notes for the skills it writes to user-level directories. It reads on-unsupported from the source root's agnostic-ai.yaml.
Put the Claude line in a fence and give other targets their own text:
::target claude
!`git log main..HEAD --oneline`
::end
::target codex
Run `git log main..HEAD --oneline` first. `$ARGUMENTS` below means the text passed after the skill name.
::end
A $1 or !`command` inside a fenced code block counts as an example and is not reported. $ARGUMENTS is reported there too. Escape a literal dollar as \$1, as Claude Code expects. import claude keeps the body as written, so Claude Code keeps its dynamic context.
Commands use the same syntax, and a few targets expand part of it there.
disable-model-invocation support by target
Only the targets listed were checked. Setting it keeps a skill out of automatic model invocation. The user can still invoke it. Omitting it leaves each target's default, which is model-invocable everywhere below.
| Target | Behavior |
|---|---|
| Claude Code, Cursor | Written to SKILL.md |
| Codex | Written as allow_implicit_invocation: false to agents/openai.yaml. An explicit value in x-codex.policy or a bundled agents/openai.yaml wins |
| Crush | Dropped with a note. Set x-crush.disable-model-invocation |
| Factory | Dropped with a note. Set x-factory.disable-model-invocation |
Crush and Factory skills land in the shared .agents/skills/ tree, so emitting the key would hand it to targets with no such field. Use the x- key. A manual-only skill that turns model-invocable crosses a safety boundary.
OpenHands' triggers is unrelated: it injects a skill on a keyword. Devin spells this restriction triggers: [user].