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.

Weak — describes the mechanism, not when to use it
---
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.

Strong — from the pinned 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").

Before — descriptive, not actionable
## 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.
After — imperative and checkable
## 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.

Before — a rule with no example
When the check finds a missing license header, add one in a style consistent with the rest of the repository.
After — the same rule, with a concrete example attached
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").

From the pinned 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.

From the pinned 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.

From the pinned 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.

Runtime detection instead of an assumption, from iru-setup-antora/SKILL.md
Run `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").

From Creating and updating skills and agents, "Naming: avoiding marketplace collisions" (abridged)
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.