How to write a Claude Code skill (SKILL.md) that actually triggers
In short: a skill triggers on its description, not its body. Say what it does and when to use it, in the third person, using the words people actually type, and put the main use case first. Keep the frontmatter to name and description and the same SKILL.md works in Claude Code, Codex and Cursor. Then test it in a fresh session with prompts that should and shouldn't trigger it.
How an agent decides to use a skill
Skills use progressive disclosure. When a session starts, the agent sees only a listing: each skill's name and description. The body of SKILL.md loads when the skill is invoked, either because you typed its name or because the model decided it fits the task. So when a skill "doesn't work", the problem is usually that listing entry, not the instructions underneath it.
- Claude Code cuts each entry's combined
descriptionandwhen_to_usetext at 1,536 characters. The whole listing gets about 1% of the context window. When it overflows, Claude Code drops descriptions for the skills you use least, and only their names remain. - Codex gives the initial skills list at most 2% of the context window (8,000 characters if the window size is unknown) and shortens descriptions first when many skills are installed.
skills.max_context_tokenschanges that budget. - The Agent Skills spec, the open standard all three tools build on, caps
descriptionat 1,024 characters.
The practical rule: front-load the key use case and trigger words, and stay under 1,024 characters so the file is valid everywhere.
Where the file goes
| Tool | Project skills | Personal skills | Run it yourself |
|---|---|---|---|
| Claude Code | .claude/skills/<name>/SKILL.md | ~/.claude/skills/ | /name |
| Codex | .agents/skills/<name>/SKILL.md, scanned from the launch directory up to the repo root | ~/.agents/skills/ | $name, or pick from /skills |
| Cursor | .cursor/skills/ or .agents/skills/ (also reads .claude/skills/ and .codex/skills/) | ~/.cursor/skills/, ~/.agents/skills/ | Type / and the skill name |
Name the folder carefully. The spec says name must match the parent folder and use only lowercase letters, digits and single hyphens, up to 64 characters. In Claude Code the folder name (or name) becomes the slash command. Anthropic's authoring guide also reserves the words "anthropic" and "claude" in skill names, so avoid names like claude-helper if you might upload the skill to claude.ai or the API later.
Anatomy of a description that triggers
The model reads descriptions to choose a skill for the current request, sometimes out of dozens. Anthropic's authoring guide and OpenAI's Codex docs give the same advice:
- What, then when. One clause on what the skill does, then "Use when..." with concrete situations.
- Third person. "Writes tests for..." rather than "I can help you write tests". The description is injected into the system prompt, and Anthropic warns that an inconsistent point of view can cause discovery problems.
- The user's words. Include phrasings people actually type ("add tests", "coverage", "this is untested") plus file types and tools (
.tsx, pytest, Vitest). - Boundaries. Codex's docs say to explain when the skill should and should not trigger. A short "Not for..." clause stops near-misses.
- Front-loaded. If descriptions get shortened, the first sentence is the part that survives.
description: Helps with tests.
description: Writes and runs unit and integration tests for changed code. Use when the user asks to add tests, improve coverage, reproduce a bug with a failing test, or says code is untested. Not for end-to-end browser tests.
A working template
Here is a complete skill. Save it as .claude/skills/write-tests/SKILL.md for Claude Code, or .agents/skills/write-tests/SKILL.md for Codex and Cursor. It uses only the two fields every tool understands.
---
name: write-tests
description: Writes and runs unit and integration tests for changed code. Use when the user asks to add tests, improve coverage, reproduce a bug with a failing test, or says code is untested. Not for end-to-end browser tests.
---
# Write tests
## Steps
1. List what changed: `git diff --name-only HEAD`. If nothing changed, ask which files to cover.
2. Find the test runner and conventions in package.json, pyproject.toml or the existing tests. Match their location, naming and style.
3. For each changed behaviour, write a happy-path test, one edge case and one failure case.
4. Run the new tests first, then the full suite.
5. If a test fails, fix the code or the test. Never delete or weaken an existing assertion to get a pass.
## Report
- Files added or changed
- Cases covered, one line each
- The exact test command you ran and its result
As it grows, keep SKILL.md as the overview and move detail into files it links to directly:
write-tests/
├── SKILL.md # required: frontmatter + steps
├── references/
│ └── fixtures.md # optional: read only when SKILL.md points to it
└── scripts/
└── coverage.sh # optional: executed, not loaded into context
Why it is shaped this way. The steps are imperative and end in an explicit output, which the Codex docs recommend. The "never weaken an assertion" line is concrete enough to check in review. Long reference material sits one level deep, so it costs no context until it is needed. Anthropic and the spec both recommend keeping SKILL.md under 500 lines.
Make side-effect skills manual-only
Some skills should never fire on their own: committing, deploying, sending messages. Each tool has a switch.
| Tool | Only you can invoke it | Only the model can invoke it |
|---|---|---|
| Claude Code | disable-model-invocation: true | user-invocable: false |
| Cursor | disable-model-invocation: true | Not documented |
| Codex | allow_implicit_invocation: false in agents/openai.yaml | Not documented |
# .claude/skills/commit-work/SKILL.md (Claude Code; Cursor reads the same field)
---
name: commit-work
description: Runs all checks, then stages and commits the current work with a Conventional Commits message. Only when the user explicitly asks to commit.
disable-model-invocation: true
---
# .agents/skills/commit-work/agents/openai.yaml (Codex)
policy:
allow_implicit_invocation: false
In Claude Code, a skill with disable-model-invocation: true is left out of the model's listing completely, so it also costs no context until you call it.
Claude Code extras, and when to skip them
when_to_use: extra trigger phrases, appended to the description and counted toward the 1,536-character cap.paths: globs that limit automatic loading to matching files. Cursor supportspathstoo.argument-hintand$ARGUMENTS(or$0,$1): arguments, as in/fix-issue 123.- A line like
!`git diff HEAD`: dynamic context. Claude Code runs the command and inlines its output before Claude reads the skill. allowed-tools: pre-approves tools for the turn that invokes the skill. The grant clears when you send your next message, and it doesn't block other tools. Read it before running skills you didn't write.context: fork: runs the skill in a subagent that can't see your conversation, so the skill must stand on its own.
These fields are Claude Code extensions, not part of the spec. Cursor documents its own small set (paths, disable-model-invocation, metadata and a few display fields), and the Codex docs list only name and description plus agents/openai.yaml. Uploading a skill to claude.ai or the Skills API fails with an "Unexpected key(s)" error if the frontmatter holds anything beyond name, description, license, compatibility, metadata and allowed-tools.
Why a skill doesn't trigger: a checklist
- The frontmatter isn't on line 1. Claude Code reads frontmatter only when the opening
---is the file's first line. Otherwise the whole file, markers included, is treated as content. - The YAML doesn't parse. Claude Code still loads the skill, but with no fields set, so
/nameworks while automatic matching has no description to use. An unquoted description containing ": " is a common cause. Wrap it in quotes. - Wrong folder for the tool. Claude Code's documented locations are under
.claude/skills; Codex scans.agents/skills. A skill in only one of them is invisible to the other tool. - The description is vague, or it describes the implementation rather than the request.
- Too many skills. When the listing overflows its budget, descriptions get dropped or shortened. In Claude Code,
/doctorestimates the listing's cost and/skill-doctorflags skills you never use. - It's switched off:
disable-model-invocation, askillOverridesentry in Claude Code settings, orenabled = falseunder[[skills.config]]in~/.codex/config.toml. - It's nested. In Claude Code, skills in a
.claude/skillsfolder below where you started load only after Claude reads or edits a file in that subdirectory.
If a skill triggers but Claude stops following it later in the session: Claude Code doesn't re-read SKILL.md on later turns, and after auto-compaction it keeps only the first 5,000 tokens of each invoked skill. Put the most important rules at the top, word them as standing instructions ("run the tests after every edit"), and move anything that must hold every time into a hook.
Test it like code
- Start a fresh session. Anthropic's docs point out that leftover context from writing the skill hides gaps in the instructions.
- Confirm it is listed. In Claude Code, ask "What skills are available?" or run
/skills. In Codex, run/skills. In Cursor, open Customize, then Skills. - Write 3–5 prompts that should trigger it and 2–3 near-misses that shouldn't. For the template above, "add a Playwright test for checkout" is a good near-miss.
- Run each prompt with the skill on and off and compare the results. In Claude Code, set the skill to
"off"inskillOverridesfor the baseline run. - Validate the frontmatter with
claude plugin validate .claude/skills(Claude Code v2.1.233 or later) orskills-ref validate ./write-testsfrom the spec's reference library.
Tune the description, not the body, until it triggers when it should. If it fires too often, narrow the description or make the skill manual-only.
One source, three tools
Because all three tools follow the same spec, a SKILL.md with just name and description runs everywhere. The catch is duplication. Cursor also reads .claude/skills and .agents/skills, so a repo that carries the same skill in several folders should keep the copies identical, ideally generated from one source. That is how our kits are built: one master source compiled into each tool's native folder and checked by a format validator. For how skills relate to rule files, see Cursor rules vs CLAUDE.md vs AGENTS.md, and for the workflow these skills support, the plan-first workflow.
FAQ
Do I need both name and description in SKILL.md?
For portability, yes. The Agent Skills spec, Codex and Cursor all require name and description. Claude Code treats every field as optional (it falls back to the folder name and the first non-empty line of the body), but the description is what Claude matches against, so always write one.
Why does my skill run with /name but never trigger on its own?
Usually the description is too vague or doesn't use the words people actually type. Other causes: disable-model-invocation is set, the YAML frontmatter fails to parse (Claude Code then loads the skill with no fields), or so many skills are installed that the listing dropped its description.
Where do Codex skills go, and how do I run one?
Put them in .agents/skills/<name>/SKILL.md. Codex scans .agents/skills in every directory from where you launch it up to the repository root, plus ~/.agents/skills for personal skills. Mention a skill with $name, or pick it from /skills.
Can one SKILL.md work in Claude Code, Codex and Cursor?
Yes, if you stick to the Agent Skills spec fields (name, description, and optionally license, compatibility, metadata, allowed-tools). Copy the same folder to .claude/skills for Claude Code and .agents/skills for Codex. Cursor reads .cursor/skills and .agents/skills, and also .claude/skills and .codex/skills.
How long can a skill description be?
The Agent Skills spec allows up to 1,024 characters. Claude Code cuts the combined description and when_to_use text at 1,536 characters in its listing, and Codex shortens descriptions when many skills are installed. Put the main use case and trigger words in the first sentence.