skill-authoring
Rules for SKILL.md files that trigger, fit, and are proven. Use when creating a SKILL.md, rewriting its description, splitting an oversized SKILL.md, or editing one because an agent ignored, misread,
SKILL.md.claude/skills/skill-authoring/SKILL.md
GitHub<!-- SYNCED COPY — do not edit here.
Canonical: common-docs/skills/skill-authoring/SKILL.md
This file is distributed to every consuming repo by
common-docs/meta/scripts/sync_skills.py. Edit the canonical, run the
sync, and commit each repo. Edits made here are overwritten and lost. -->
skill-authoring — skills that fire, fit, and are proven
A skill is a guard on agent behavior, so fix-the-class applies: a skill you never watched
fail-then-pass is not a guard. These rules sit on top of the docs skill (whole-document review, never lose a rule) — read
that too.
1. The description is the trigger — and the listing is capped
How Claude Code loads skills (official docs, checked 2026-09-10):
- The model sees only each skill's
name+description(+when_to_use) until it invokes the skill. That combined text is truncated at 1,536 chars per skill in the listing (skillListingMaxDescCharschanges the cap). - The whole listing is budget-capped at 1% of the context window (
skillListingBudgetFraction, orSLASH_COMMAND_TOOL_CHAR_BUDGETas a fixed char count, raises it). The listing always keeps every skill name; on overflow Claude Code drops descriptions starting with the skills invoked least, and askillOverridesentry of"name-only"drops one on purpose. A skill without its description can still be invoked but is rarely chosen on its own — every character you add can push another skill's trigger out. - Measured in the Claude Code 2.1.263 source, beyond the docs: 4 chars/token (8,000 chars at 200k,
~40,000 at 1M); every entry still costs its name + 2 chars; descriptions are admitted greedily by
usage score (use count, 7-day half-life). So every duplicate entry (a synced copy listed as
aidream:x) burns budget even with no description. - Skills in
.claude/skillsof subdirectories below the launch dir load lazily, the first time a file there is read or edited. That is why repo-scoped duplicates (aidream:x,matrx-frontend:x) appear mid-session, each one another listing entry. disable-model-invocation: trueremoves the description from context entirely: the skill becomes user-invoked only (/name), and subagents cannot preload it either.
Rules:
- Recipe:
<what it is — one noun phrase>. Use when <trigger>, <trigger>, … .Primary use case first. Target ≤300 chars, hard 500 — our rule, stricter than the docs, which set only the 1,536 cap and say to put the key use case first. - Triggers are what the agent will actually see: situations, symptoms, file paths/globs, error strings, the user's words.
- Never the procedure. No "Covers / Encodes / Enforces / pipeline", no step lists — an agent handed the procedure in the description executes the summary and skips the body.
- No dates, rulings, approvals, or keyword dumps — those live in the body. Cutting a dump is a MOVE, not a deletion: every trigger you drop from the description gets a row in a relocation table in the body — old trigger → its new home — an index a repo grep still finds. That table is what "don't lose a trigger" actually asks for, and it is what you show the owner.
- At most one
NOT for X (use Y), only when a sibling skill is a real near-miss. - Slash-only skill (reached by
/nameand by nothing else) →disable-model-invocation: true. Firstgrep -r "<name>"across all skills and agent definitions — no other skill or subagent can reach it afterwards. - The lint enforces it:
python3 common-docs/meta/scripts/skill_descriptions.py lint(--repo <dir>,--workspace) fails a description over 500 chars unless allowlisted and over 1,024 always;skill-description-allowlist.txtonly shrinks. 1,024 is a hard ceiling, never a target — the house target is ≤300, and every proven-good rewrite on record landed at 276–300. Writing up to a cap is the failure this rule exists to stop. Wired intookf_lint.py, aidreamscripts/check_skill_descriptions.py, and matrx-frontendpnpm check:skill-descriptions. - A trigger that keeps missing in a footgun area → run the
skill-creatorplugin's description optimization loop (should-fire + near-miss queries), never lengthen by hand.
2. Size — the body routes over branches
- SKILL.md body ≤500 lines — the file itself, not the directory. Over budget is fixed by relocating, not by squeezing prose.
- Branch test: inline what every run needs; move what only some runs reach (one stage, one
provider, one variant, a long API reference) into a sibling file named for the branch, with a
pointer that says when to read it:
Stage V only → read stage-v.md. - A routing list is read as complete. A "read only the branch your run reaches" list names
every companion, pre-existing ones included; an unlisted file is never opened (matrx-frontend
surface-authoringevals E1: 0/3 reps opened the one reference the list omitted and lost its completion gate). Never end a route with "nothing else": pointers inside a branch file (e.g. "run thesafe-cutoverskill first") still bind, and 2/3 common-docsdata-to-kindsreps skipped one citing those words. - One level deep. SKILL.md → file, never file → file. A reference file over 100 lines opens with a contents list.
- Point at
--helpor a script instead of restating flags or deterministic steps. - Splitting relocates; every rule survives. Canonical SKILL.md companion files ship with
sync_skills.pyautomatically (in common-docs they carry OKF frontmatter). - A skill names its home and never holds a rule by hand (law 15). Frontmatter
subject: <node>points at the subject's one home doc; the home's rules are stamped in byone_home.py --write(docs §9). The skill body keeps the how — steps, triggers, traps — and a rule you want to add goes into the home. One skill per subject part (server, client), never two skills for one part; before writing a new skill,rg '^subject: <node>'for the ones that exist. - Every step ends on a checkable completion criterion — "every touched table returns
canonical_certify_ok", never "understanding reached".
3. Match the form to the failure
| Observed failure | Write | Never write |
|---|---|---|
| Knows the rule, skips it under pressure | Prohibition + named replacement + §4 counters | Soft advice ("prefer", "consider") |
| Complies, but output has the wrong shape | A recipe: what the output IS, its parts, in order | A list of don'ts — they get negotiated |
| Omits a required element | A REQUIRED slot in the template it fills | A prose reminder near the template |
| Behavior depends on a condition | A conditional on an observable predicate | A rule plus an exemption clause |
No nuance clauses ("unless it matters") — they reopen the negotiation. A real exception is its own conditional.
4. Discipline skills — close the loopholes you saw
A discipline skill guards a rule that costs the agent time and that it already knows (verify live, never revert a gate, never silence a type error).
## Rationalizations— a two-column table,Excuse (verbatim) | Reality (one line). Rows only from observed transcripts or real incidents; an imagined row is bloat.## Red flags— 3–7 thoughts that precede the violation ("this case is different", "the gate is blocking my release").- Put the about-to-violate symptom in the description:
Use when … or when tempted to ….
5. Prove it
Required for a new skill, an edit made because an agent misbehaved, any discipline skill, and any description rewrite. Exempt: typo, path, and pointer fixes.
- Scenario from reality. Use the prompt or transcript that actually failed (incident, feedback item, review-queue row). Only with no incident, synthesize one with real repo paths, ≥2 real pressures (time, sunk cost, "the user said skip it", a blocking gate), and a forced choice.
- RED — baseline. Dispatch fresh subagents (lane named;
standardunless the consuming lane is known) with the scenario and without the new text (for an edit: the current skill). 3 reps. Record choices and rationalizations verbatim. No failure → nothing to fix; do not write the guidance. - GREEN. Write the minimum that answers the recorded failures. Same scenario, lane, 3 reps, with the skill. Pass = all 3 comply and cite the skill.
- REFACTOR. A new rationalization → a §4 row + red flag → rerun. Reps that disagree with each other mean the wording is not binding — change the form (§3) before adding words.
- Trigger check (description changed): 3 should-fire and 3 near-miss prompts to fresh subagents with the skill installed; record fire / no-fire.
- Record the proof in
<skill>/evals.md: scenario, lane, date, RED result, GREEN result, rationalizations harvested. It is the regression test — the next editor reruns it. The author never grades a run it performed itself. Every recorded rep cites its agent id or transcript path — a verdict nobody can re-check against the transcript is a claim, not proof; when a rep's transcript genuinely cannot be recovered, its row says "transcript not recoverable" rather than looking auditable. (Two records shipped without ids on 2026-09-10 and had to be reconstructed from runner transcripts two days later.)
Before you save — checklist
-
subject:names the home; no rule written here by hand;one_home.pyexits 0. - Description follows §1: noun phrase +
Use when, ≤300 chars (500 hard), no procedure, no dates;skill_descriptions.py lintpasses. - Slash-only?
disable-model-invocation: trueafter the cross-skill grep. - Body ≤500 lines; branch-only material disclosed one level deep with when-to-read pointers; any routing list names every companion file.
- Every rep in
evals.mdcites an agent id / transcript path, or says it is not recoverable. - Split a SKILL.md? Every original line survives somewhere in the directory (re-grep or an oracle), an agent that did not split it verifies routing, and a §5 scenario run proves no behavior was lost.
- Guidance form matches the observed failure (§3).
- Discipline skill: rationalization rows and red flags come from observed failures.
- §5 run done (or exempt) and
evals.mdupdated. -
docsskill §2 and §8 passed. Canonical SKILL.md? Edit the one undercommon-docs/skills/— never a synced copy — runsync_skills.py, commit every touched repo.