Writing Effective Skills
|
This content was generated with the assistance of AI and should be verified against the official documentation of each tool and framework before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Examples on this page target Claude Code, checked against the official Claude Code documentation as of
2026-09-28 — the version baseline checked that date is VS Code 1.139 with the GitHub Copilot Chat extension,
GitHub Copilot CLI 1.0.88 and Claude Code 2.1.283. The worked examples are excerpted from this account’s
ai-catalog repository, pinned to commit
58fb709
(58fb70954404f9d539031a02ac6d4413f03745be) so every link and excerpt below points at exactly the same
snapshot. The advice applies to any Agent Skills SKILL.md: GitHub Copilot reads the same format (including the
.claude/skills/ folder), though a few frontmatter keys and tools used below, such as AskUserQuestion, are
Claude Code-specific. Skills covers what each tool supports, and the
same discipline (direct wording, one job, a stated stopping point) carries over to a Copilot
.github/prompts/.prompt.md file or .github/agents/.agent.md custom agent.
Descriptions written for triggering
A skill’s description (with its name and any when_to_use text) is the only part of it that is always in
context, for every installed skill Claude may invoke, on every turn — Claude matches your request against every installed skill’s description to decide which one (if
any) to invoke on its own, before it has read a single line of the skill’s body. A description that only says
what the skill does, without saying when to reach for it, competes poorly against neighbouring skills that do
both. Claude Code’s own guidance is to put the use case first and to write out the trigger phrases a user would
actually type, not just an abstract summary of the mechanism — see
Claude Code — Skills and
Anthropic — Agent Skills best practices: Writing effective
descriptions, which also asks for descriptions written in the third person.
A useful description answers four questions, ideally all in one or two sentences: what does invoking it
actually do (a command run, a file written, a decision made), how is it invoked and with what inputs — its
invocation form, the literal slash command a user types plus any arguments it takes (an issue number, a path,
nothing at all), such as /iru-check-license <path-or-glob> — what is explicitly out of scope (so Claude does
not reach for it on a request that only looks similar), and, where two skills could plausibly both match the same
request, "Use whenever … instead of …" to disambiguate them.
---
name: iru-check-license
description: Checks license headers in source files.
---
This tells Claude what the skill touches but not when a request should trigger it, what it takes as input,
or how it differs from a skill that, say, adds a brand-new LICENSE file. Two requests that should clearly go
to different skills — "add an MIT license to this repo" and "make sure every file has the right license
header" — would both plausibly match this description equally badly.
iru-check-license/SKILL.md---
name: iru-check-license
description: Check for a license file (LICENSE, LICENSE.txt, LICENSE.md, COPYING, etc.) in the repository root, and if one exists, verify every source and test file carries a header consistent with it. Learns the header format from files that already have one; if none carry a header yet, generates a standard header for that license type. Invoke as `/iru-check-license` to scan the whole repository, or `/iru-check-license <path-or-glob>` to scope the check to specific files/directories. Use whenever the user wants license-header compliance checked or backfilled, instead of eyeballing individual files.
model: haiku
---
This excerpt demonstrates every ingredient at once: the invocation form is spelled out literally
(/iru-check-license, with and without a scoping argument), the precondition that makes the skill a no-op is stated up
front ("if one exists"), and the closing sentence is a direct "use whenever … instead of …" that names the
manual alternative ("eyeballing individual files") this skill replaces. Generalised, a description worth
writing follows the same shape: [what it checks or does] + [the precondition or scope that narrows it]
[the invocation form] + "Use whenever X, instead of Y."
Imperative, checkable steps
A SKILL.md is read and executed by an agent, not interpreted by a person with judgment to fill the gaps — wording that only describes intent leaves the actual action to be reinvented on every run, and reinventions
drift. Phrase each step as an instruction to carry out, and make it possible to point at the exact step where
something went wrong ("it stopped at Step 3").
## Step 2
This step is about making sure the repository doesn't already have Antora set up, since we don't want
to clobber anything that's already there.
## Step 2 — Survey what already exists
Before creating anything, check:
- Does `docs/antora.yml` already exist?
- Does `docs/antora-playbook.yml` exist?
- Does `docs/modules/ROOT/nav.adoc` exist, and if so what pages does it already list?
Build a concrete gap list from this survey. Every later step acts only on genuine gaps.
The "after" version, adapted from the pinned
iru-setup-antora/SKILL.md,
gives a checklist an agent executes literally instead of a paragraph it has to translate into action first — the same "imperative, not descriptive" rule this account’s own
Authoring effective instructions guide states for CLAUDE.md, SKILL.md and
agent prompts alike. Anthropic’s guidance frames the same idea as numbered workflows with a checklist the agent
ticks off — see
Anthropic — Agent Skills best practices: Use workflows for
complex tasks.
Examples over rules
A rule stated only in the abstract ("format commit messages consistently") leaves an agent to invent its own interpretation of "consistently" every run; a worked example removes that ambiguity because there is nothing left to misinterpret.
When the check finds a missing license header, add one in a style consistent with the rest of the repository.
When a file is missing a header, insert the template header as the very first content in the file, followed
by a single blank line, then the file's existing first line unchanged. For example, given the license
identified in Step 1 is Apache-2.0 and the learned template is:
/*
* Copyright 2026 Acme Corp.
* Licensed under the Apache License, Version 2.0.
*/
insert it above `package com.example.docsassistant;`, not above an existing Javadoc comment.
An example can be given inline, as above, when it is short, or by reference — pointing at a real file already in the repository and instructing the agent to read it before proceeding — when it is long or only sometimes relevant. Both forms, and the reasoning for choosing between them, are covered in full in Authoring effective instructions's "Give examples, not just rules" section; see also Anthropic — Agent Skills best practices: Examples pattern.
Explicit stop conditions and HITL points
An unattended skill should keep interruptions rare by design, but the moments it does need a person are worth
stating as explicitly as its happy path — otherwise an agent either silently guesses at a decision only a
human can make, or interrupts far more often than it needs to. Claude Code’s AskUserQuestion tool is the
mechanism for that: it presents the person with a small set of concrete options (rather than an open-ended
question) and blocks on their answer before the skill continues — see
Claude Code — Tools reference. It is available only in the main
conversation: subagents don’t get AskUserQuestion, so a skill run inside one (for example through a
context: fork or an executor agent) has to report the decision back instead of asking — see
Claude Code — Subagents ("Available tools").
iru-code/SKILL.md, "Step 6 — When to interrupt the user" (abridged)Keep interruptions rare — by design, this skill should run start-to-finish unattended on a well-formed plan.
Stop and use `AskUserQuestion` (or plain text if no real choice is being offered) only when:
- The `iru-code-one-task-group` skill invoked in Step 4 reports that one of the group's tasks stopped on a blocker —
a test that keeps failing after a genuine fix attempt where the failure suggests the plan's described approach
is actually wrong or infeasible against the real code, or an ambiguity that changes correctness or scope and
can't be safely inferred (conflicting instructions, a choice only the user can make, a missing decision).
…
- A tool call needs a permission outside this skill's `allowed-tools` list and the underlying action is risky or
irreversible …
Do not stop merely because a task is nontrivial — implement it. …
This matters for iru-code in particular, because iru-issue does not run it inline: it hands the whole run to
the iru-isolated-skill-executor agent, and inside that subagent AskUserQuestion is not available. Each stop
condition above therefore has two outcomes: in the main conversation the skill asks, and inside the executor it
either proceeds with the documented default (where the decision has one) or stops and reports the blocker back,
which the executor returns to iru-issue as a blocked outcome for the main conversation to put to the person.
Generalised, a skill’s stop conditions are worth stating as a short, closed list — not "ask when unsure," but the specific situations that qualify (an irreversible or destructive action, a choice with no best-practice default, a failure that suggests the plan itself is wrong) — paired with an explicit non-example ("do not stop merely because X") so the bar for interrupting stays narrow and consistent across runs. For the related idea of building a validate-fix-repeat loop into the steps, so the skill stops itself only after a check passes, see Anthropic — Agent Skills best practices: Implement feedback loops.
Idempotency and resume
A skill invoked more than once against the same repository — because the first run was interrupted, or because it is meant to be safe to re-run as the project evolves — should not blindly redo or duplicate work it already did. The reliable way to guarantee that is a dedicated early step that surveys what already exists before anything is created or changed, so every later step acts on a concrete gap list instead of an assumption of a clean slate.
iru-setup-antora/SKILL.md, "Step 2 — Survey what already exists" (abridged)Don't assume a clean slate. Before creating anything, check:
- Does a `docs/` directory already exist at the repository root? If yes, is it already an Antora module (does
`docs/antora.yml` exist)?
- Does `docs/antora-playbook.yml` (or another playbook file referenced from `docs/package.json` scripts) exist?
- …
- Does `docs/modules/ROOT/` exist, and if so what pages does `docs/modules/ROOT/nav.adoc` already list?
- …
Build a concrete gap list from this survey (…). Every later step acts only
on genuine gaps — a fully-set-up repository (one that already has all of this) should result in Step
13 reporting "already set up," not in duplicated or overwritten files.
This is what the skill’s own description calls out as "Idempotent: safe to re-run on a repository that
already has some or all of this in place — it fills gaps without overwriting existing pages, config, or nav
entries." Generalised: give a resumable skill a "check existing state first" step, have every later step
condition on that step’s findings rather than assume the target starts empty, and state that idempotency
explicitly in the description so the person invoking it (and Claude, deciding whether to invoke it again)
knows re-running is safe. Anthropic’s guidance recommends the same shape for multi-step skills: a workflow whose
steps are tracked in a checklist, so progress survives a partial run — see
Anthropic — Agent Skills best practices: Use workflows for
complex tasks.
Pass-through key: value args
A skill meant to be driven both interactively and by another orchestrating skill needs a way to accept
answers to its own questions up front, so the orchestrator does not have to sit through prompts it already
knows the answers to. This catalog’s iru-setup-* skills use a plain-text args block of key: value lines
for exactly this, parsed at the top of the skill before any question is asked.
iru-setup-java-library-repository/SKILL.md, "Step 0 — Resolve inputs" (abridged)This skill can be invoked stand-alone (`/iru-setup-java-library-repository`) or driven by another orchestrator, so
parse `args` first, as `key: value` lines, one per line, e.g.:
mode: new
open-source: yes
publish: yes
sonar: cloud
sonar-organization: example-github
sonar-project-key: example_my-library
sonar-host-url: https://sonarcloud.io
Recognized keys: `mode` (`new` or `existing`), `open-source` (`yes`/`no`), `publish` (`yes`/`no`), `sonar`
(`cloud`/`self-hosted`/`none`), and — only when `sonar` is `cloud` or `self-hosted` — `sonar-organization`,
`sonar-project-key`, `sonar-host-url`. Every key found here is resolved — skip the matching question in Step 2.
Only keys genuinely missing from `args` still need asking. If `args` is absent or doesn't look like this format,
treat everything as unset and ask normally.
The caller passes those lines as the Skill tool’s args string, the same way ai-catalog’s orchestrators
dispatch their children:
Skill({skill: "iru-setup-java-library-repository", args: "mode: new\nopen-source: yes\npublish: yes\nsonar: none"})
Typed by a person, the same text simply follows the command name and reaches the skill as $ARGUMENTS — see
Claude Code — Skills ("Pass arguments to skills").
Generalised, a skill built to be composable this way needs three things: a documented, flat key: value
contract for its recognised inputs (stated in its own frontmatter description or an early step, so a caller
does not have to read the whole skill to learn it); a rule for what happens when a key is present versus
missing (skip the question versus ask it); and a graceful fallback (treat malformed or absent args as
"nothing supplied," never as an error) so the skill still works invoked directly by a person typing its slash
command with no arguments at all.
Runtime detection over hardcoding
A skill that assumes one specific language, framework, license or directory layout breaks the first time it
runs against a repository that does not match that assumption, and silently produces wrong output rather than
failing loudly. The pinned catalog’s setup and check skills consistently discover facts about the repository
at run time instead: iru-check-license states plainly that it "makes no assumptions about this being any
particular repository or language — discover the license and the header convention fresh each run," reading
whichever license file and existing header style are actually present rather than assuming Apache-2.0 and a
Java /* … / comment; iru-setup-antora runs node -v and npm -v to check what is actually installed
rather than assuming a version; and, throughout the catalog’s iru--code-one-task-group dispatch (covered in
Orchestration Patterns), a task’s declared
language/framework key is looked up against whichever <key>-code-one-task-group skill happens to be
installed, rather than a fixed list baked into the dispatching skill. The practical rule: wherever a skill’s
correct behaviour depends on a fact about the environment or the target repository — an installed tool’s
version, an existing file’s format, which sibling skills are actually present — read that fact at run time
instead of writing today’s value into the skill’s instructions.
iru-setup-antora/SKILL.mdRun `node -v` and `npm -v`.
Claude Code can also do the detection before the model sees the skill: a !`command` placeholder in the body is
run when the skill loads and replaced by its output, so the instructions arrive with the live value already filled
in (for example - Node version: !`node -v`). An injected command that fails aborts the whole invocation, and,
outside auto mode, so does one that would need a permission prompt unless allowed-tools pre-approves it. See
Claude Code — Skills ("Inject dynamic context") and
Anthropic — Agent Skills best practices: Avoid assuming
tools are installed.
Naming and namespacing
Every skill in the pinned catalog carries an iru- prefix on its directory name, its frontmatter name, and
every cross-reference to it — not a style preference, but a namespace. A single repository’s
.claude/skills/ directory can hold skills hand-copied from more than one source at once (this catalog, another
team’s skills), and personal and project skill directories are merged into one / namespace. Two skills that both
end up named plan can’t coexist in one directory, and across scopes only one runs: a personal skill beats a
project skill of the same name, and any skill beats a same-named .claude/commands/ file. Only plugin skills are
namespaced automatically (/plugin-name:skill-name). See
Claude Code — Skills ("Resolve skills that share a name").
Every skill and agent this catalog ships carries an `iru-` prefix — … A repository can install
skills from more than one source at once (this catalog, a plugin marketplace, another team's hand-copied
skills) into the same `.claude/skills/` directory, and slash commands are matched by directory name alone —
two differently-sourced skills both named `plan` or `code` would silently shadow one another. The prefix is
this catalog's namespace, so `/iru-plan` never collides with an unrelated `/plan` a consuming repository
picked up elsewhere.
The guide simplifies the lookup (a skill’s frontmatter name can also set its command, as described above), but
the collision risk it guards against is real. It is also explicit that the prefix is a namespace, not a
requirement of the underlying mechanism: a single skill adopted into another repository that doesn’t use this
convention is free to drop the prefix, because it exists to keep the catalog’s own skills from colliding with each
other and with other sources once several are installed side by side. The rule that generalises is:
a skill meant to be shared or installed alongside skills from other sources needs a short, consistent prefix
of its own — lowercase, hyphen-joined, applied to the directory name, the frontmatter name, and every
/name reference to it — chosen once and kept, not added after a collision has already happened. Keep the prefix
in the directory name and name itself, not as a myorg: or myorg/ namespace: Copilot requires name to match
the directory and use only lowercase letters, digits and hyphens, and silently skips a skill whose name doesn’t. See
Anthropic — Agent Skills best practices: Naming conventions and
VS Code — Agent Skills.