- Docs
- Guides
- Cross-cutting
- How-to
- How to author a skill
How to author a skill
Create a lint-clean skill that follows the catalogue's structure, dependency, script, and evaluation standards.
Use this when: You are authoring a new skill — writing the SKILL.md, scripts, and evals — and need to meet the catalogue’s structural and lint-enforced standards.
Prerequisites: A skill directory under packs/<pack>/.apm/skills/<name>/ with a SKILL.md; see “Before you start”.
Result: A lint-clean, catalogue-standard skill with correct frontmatter, body structure, dependency handling, and evaluation fixtures.
This guide collects the standards every skill in this catalogue follows — the bundled packs and the skills you author in your own pack alike. Some standards are checked by lint (called out inline, with the linter as the authority); the rest are reviewer-enforced. It assumes you already know what a skill is and when to add one (docs/CONVENTIONS.md § Skills: you’ve done the same multi-step thing three times, and you’re not adding one speculatively).
If you’re authoring the first skill in a new pack, read Pack workflow design first — it tells you how to design the pack’s arc before writing individual skills.
For the credential dimension — a skill that calls an authenticated API or holds a token — read How to add a credentialed skill alongside this; that contract is separate and more specific.
Before you start
Section titled “Before you start”You need a skill directory under packs/<pack>/.apm/skills/<name>/ with a SKILL.md, and — if it does real work — a scripts/ helper.
Frontmatter and description
Section titled “Frontmatter and description”Keep frontmatter to the keys the agentskills.io spec allows (name, description, license, compatibility, metadata, allowed-tools) and put any project-specific data under the metadata: escape hatch. The description is one sentence that names the trigger (“Use when …”), not the implementation. name must be kebab-case (^[a-z0-9]+(-[a-z0-9]+)*$, 1–64 characters); the linter enforces this.
The description must be a single-line scalar. Folded/literal YAML blocks (>, |) and continuation lines (an indented next line) parse as valid YAML but break on some adapter targets — their downstream loaders fail on a multi-line description even when the YAML itself is clean. For the same reason, keep YAML structural characters out of an unquoted value: a bare : mid-description, a leading #/&/*/[, or whitespace-then-# all change the parse. If you need any of those characters, wrap the whole value in double quotes. Keep it under 1024 characters — Kiro’s frontmatter parser silently truncates at the byte boundary, cutting descriptions mid-sentence for Kiro users.
Don’t memorize the exact rules from this page — run the linter, which is the source of truth:
agentbundle catalogue lint --root . --deepIt checks the key whitelist, description syntax, allowed-tools shape, evals/, and the projected copy. (The CONVENTIONS rule applies: the linter does the style job better than prose can.)
Body structure
Section titled “Body structure”Use the section order the shipped skills share, so a reader always knows where to look:
# <Skill> title+ a 2–3 sentence intro stating what it does and what it doesn’t.## When to Use— the trigger conditions.## Prerequisites— tools, packages, sibling skills, credentials (see the tier ladder below).## Instructions/## Procedure— numbered steps with copy-paste commands. The agent invokes the script; it is not the script.## Pitfalls/### Don't— known failure modes and what not to do.## Verification— how the agent confirms it worked.
Naming your skill
Section titled “Naming your skill”Use this verb taxonomy to pick the right name:
| Verb | Meaning | Activation phrasing |
|---|---|---|
status | Orient — “where am I / what’s next?” | Cold-start phrases, “what’s on today”, “orient me” |
start | Create/begin a sustained project | “start a research project”, “kick off an investigation” |
check | Quality/health read — “is it good / saturated / done?” | “is this ready”, “should I keep gathering” |
init | Repo-scaffold only | init-project, adapt-to-project; cf. git init |
resume | Return to prior work | Activation phrase — not a skill name; see work-loop’s argless trigger |
Banned as skill names: arrive, orient, onboard, return, onboarding — these are UX-stage labels, not user-facing commands.
Directory layout
Section titled “Directory layout”A skill is a directory with SKILL.md plus four optional subdirectories — and the linter only blesses those four:
scripts/— helper code the skill invokes (python scripts/foo.py). The skill body drives the script; it is not the script.references/— detailed material the agent loads on demand, not every time (schemas, per-branch strategies, long tables).assets/— templates and fixtures the skill copies or fills in (assets/template.html,assets/state.json).evals/— evaluation fixtures. Two files serve two tiers:evals/eval_queries.json(Tier-A activation evals) and/orevals/evals.json+evals/files/<fixture>(Tier-B output-quality evals). See Evals below.
Tests are not one of them. .apm/ is the runtime export boundary — a test for
scripts/foo.py goes in packs/<pack>/tests/skills/<name>/, outside the payload it
validates, and imports or executes the real implementation. Never scripts/tests/,
even though the installer would ignore it. evals/ is not a test directory and
stays put: a fixture only means anything beside the skill it exercises, so it is
skill-local and projects with the skill. Full rules in
catalogue authoring standards § 4.
Three rules to get right the first time (the first two are linter-enforced):
- Keep files one level deep. A reference to
scripts/a/b/c.pywarns — flatten it. (evals/keeps its own canonical nesting.) - Reference your own files skill-relative, sibling skills by name.
scripts/foo.pyandreferences/bar.md, never.claude/skills/<name>/...orpacks/<pack>/.apm/skills/<name>/...install-path prefixes; to point at another skill, name it (the jira skill), don’t path into it. - Each skill is self-contained. Never read from, import from, or assume the presence of files in another skill’s directory — including sibling skills in the same pack. Skills are projected independently to each adapter; cross-skill paths that look valid in the source tree do not survive projection. If a skill needs runtime helper code, place it in the skill’s own
scripts/directory — the projection copies the full skill directory tree includingscripts/. The.apm/shared-libs/path is a specialised rail for the credential-broker companion shim; it is not a general-purpose shared-code mechanism and must not be used for skill code.
Edit the seed under packs/<pack>/.apm/skills/<name>/, never the projected copy under .claude/skills/; after any edit run make build-self to regenerate the projection, then agentbundle catalogue lint --root . --deep.
Progressive disclosure
Section titled “Progressive disclosure”SKILL.md is the always-loaded entry point; everything in references/ is pulled in only when the workflow reaches it. So keep SKILL.md lean — the spec recommends under 500 lines, the linter warns past 500 and errors past 1000 — and push depth into references/ that the body links to at the moment of need. file-to-markdown is the model: its body routes to exactly one of five references/strategy_*.md files per run (“Pick one strategy and load its reference file … Do not load any other strategy file unless you switch”), so the agent never carries four irrelevant strategies in context. Reach for a reference file whenever a section would otherwise bloat the body with detail only one branch needs.
Write scripts cross-platform
Section titled “Write scripts cross-platform”Skills run under every adapter and on every OS, including Windows, where there’s no guaranteed POSIX shell — PowerShell is the default and git-bash may not be present. So:
- New helper scripts are Python, not bash. Invoke them as
python scripts/<name>.pyfrom the skill body — portable everywhere. - Detect tools with
shutil.which("<tool>"), notcommand -v.command -vis a POSIX-shell builtin that doesn’t exist in PowerShell (Get-Commandis the equivalent); a Python check resolves identically on macOS, Linux, and Windows. - Prefer
pathlib,tempfile.gettempdir(), and Python-level filtering overgrep/sed/findand hardcoded/tmp. Declare an OS restriction only when the dependency is genuinely platform-bound. - Always pass
encoding="utf-8"toread_text/write_text/openfor text. Windows defaults to CP1252 and silently corrupts non-ASCII Markdown/JSON/TOML otherwise. subprocessuses list form, nevershell=True, and never shells out towhich/grep/find/sed/awk/make/bash— use the Python equivalent.
Dependencies: the three-tier policy
Section titled “Dependencies: the three-tier policy”Any dependency outside the Python standard library — a CLI binary, a pip/npm package, or a sibling skill — falls under one of three tiers. Tier 1 is mandatory and the default; Tier 2 is allowed but never the default; Tier 3 is banned.
What counts as a dependency: Python itself is the assumed runtime — skills invoke python scripts/..., so a stdlib-only script declares nothing. pip/uv ship with a Python install, so a pip-based Tier-2 install is low-risk (the manager is almost always there). npm/Node is not a given — it’s only present if the user installed Node — so an npm-based skill detects npm (and Node) before relying on it, and a Tier-2 npm install is gated on that detection. Prefer stdlib over a pip dependency, and a pip dependency over an npm one, when you have the choice.
Tier 1 — declare, detect, fail clean (mandatory, default)
Section titled “Tier 1 — declare, detect, fail clean (mandatory, default)”- Declare. A
## Prerequisitessection names the tool, the minimum version if one matters, and the exact install command or link. - Detect. The first action of any workflow checks the dependency is present, before any real work — a
--checkverb on your helper script that usesshutil.which("<tool>")and exits0(present) /2(absent). (shutil.whichfinds binaries; probe a library package with an import /require.resolveinstead — see the variations below.) Where a version floor matters, also parse<tool> --versionand compare. - Fail clean. On absence, stop and emit a precise remediation string — the exact install command from
## Prerequisites. Do not improvise around the missing tool (no hand-rolled fallback, no third-party web service).
mermaid-renderer is the reference:
## Prerequisites
The renderer shells out to `@mermaid-js/mermaid-cli`. Install once:
```bashnpm install -g @mermaid-js/mermaid-cli```
### Step 1 — Verify `mmdc` is available
```bashpython scripts/render_mermaid.py --check```
- Exit code 0 → `mmdc` is on `PATH`, proceed.- Exit code 2 → not installed. Tell the user the install command above. Don't try to install it for them.Tier 2 — opt-in, gated, idempotent install (allowed, not default)
Section titled “Tier 2 — opt-in, gated, idempotent install (allowed, not default)”A skill may install a dependency for the user only when all of these hold:
- the install is a single, deterministic command;
- it needs no sudo / root;
- it uses a package manager the user demonstrably already has —
uv,npm,pipx, orbrewif detected (detect the manager itself first; don’t assume it).
When you install, the pattern is always detect → install → re-verify, and you pin the version — never assume the install succeeded:
1. detect: shutil.which("foo") → present? done, no-op.2. gate: absent → ask the user; install only on explicit consent.3. install: npm install -g foo@1.2.3 (pinned, no sudo)4. re-verify: shutil.which("foo") → still absent? fail clean.Gated means the user opts in: get explicit consent before installing into their environment; don’t install as a silent side effect of the first run. Idempotent means re-running is safe — because you detect first, an already-present dependency is a no-op.
Tier 3 — banned
Section titled “Tier 3 — banned”Never:
- silently auto-install anything (install without the user opting in);
- assume sudo / root, or run an install that needs it;
curl … | bash(or any pipe-to-shell installer) without explicit consent;- assume PATH within the same session — re-verify after an install rather than trusting that a just-installed binary is resolvable.
Variations on Tier 1 detection
Section titled “Variations on Tier 1 detection”- A pip/npm package, not a binary. Probe presence with an import /
require.resolve, notshutil.which(which finds binaries, not library packages); on absence, declare the install line and stop —file-to-markdownis the detect-and-stop model. A skill that goes further and installs on consent is Tier 2, not Tier 1 (markdown-to-html). - A sibling skill. Detect by invoking that skill’s own
checkverb and reading its exit code; on failure, point the user at the sibling’s setup rather than reaching into its internals (flow-metrics, in the atlassian pack →jira: check). - A vendor CLI the user authenticated (
gh,git,kubectl). Presence detection still applies; the credential dimension is theauth: clibroker — see the credentialed-skill guide.
Evals — does the skill activate, and does it do the job?
Section titled “Evals — does the skill activate, and does it do the job?”A skill has two failure modes worth measuring, and evals/ holds a separate
file for each. They are distinct files with distinct schemas — don’t merge
them.
| File | Tier | Question it answers | Run by |
|---|---|---|---|
evals/eval_queries.json | A — triggering | Does this skill activate on the prompts it should, and stay quiet on the near-misses it shouldn’t? | tools/run-pack-evals.py (today) |
evals/evals.json | B — output quality | Once activated, does it do the job? | authored by hand now; automated running/grading is deferred to a future RFC |
Tier A — writing activation evals (evals/eval_queries.json)
Section titled “Tier A — writing activation evals (evals/eval_queries.json)”A flat JSON array; each element is { "query": "<a natural user prompt>", "should_trigger": <bool> }. Aim for ~8–10 should-trigger and ~8–10
should-not-trigger cases. The negatives are the load-bearing part: make them
near-misses — prompts that share keywords or concepts with your skill but
need a different one — not trivially-irrelevant prompts. (For the Office
converters, the negatives deliberately separate docx / pptx / xlsx from each
other; for new-spec, “record this decision” is a near-miss that belongs to
new-adr.)
[ {"query": "Let's write a spec for a new export feature", "should_trigger": true}, {"query": "Fix the bug where export drops the header row", "should_trigger": false}]Declare which skills are covered in the pack’s pack.toml — an explicit
allowlist, never auto-discovery:
[pack.evals]skills = ["new-spec", "bug-fix"]Then run the evals locally (report-only; needs the claude CLI on PATH):
python tools/run-pack-evals.py --pack <name>It projects the pack in isolation, runs each query through the headless claude
detector several times, computes a per-query trigger_rate, and grades it: a
should_trigger: true query passes iff trigger_rate > 0.5; a
should_trigger: false query passes iff trigger_rate < 0.5. The runs and a
bounded summary.json land in a gitignored, iteration-numbered eval-workspace
(see pack layout). A miss is a signal to
sharpen your description: — the one field that drives activation.
Not every skill belongs in [pack.evals].skills: a reviewer-internal skill with
no user-prompt surface (e.g. security-checklists), or one loaded broadly by a
discipline rather than a narrow prompt (e.g. work-loop), is deliberately left
out.
Running Tier-A evals in-harness (Kiro IDE, or without the claude CLI)
Section titled “Running Tier-A evals in-harness (Kiro IDE, or without the claude CLI)”The headless mode above needs the claude CLI. Where you don’t have it —
Kiro IDE, or an interactive session where you’d rather not shell out — there
is a second, lower-fidelity mode. The host agent
itself is the detector: for each query it dispatches a fresh, read-only
sub-context (Claude Code’s subagent; Kiro’s agent-spawn) given the covered
skills’ description:s, and asks which it would activate. Drive it as a
procedure:
- For each covered skill’s
eval_queries.json, dispatch one sub-context per query — judgement only; it must not run any tool or skill body (the query string is author-influenced and the headless--allowed-tools Skillsandbox does not apply here, so containment is yours to enforce). Two controls make that a floor, not a hope:- Dispatch read-only / no-execution. Use the most restricted dispatch
the harness offers (a read-only subagent type, or an explicit
“use no tools” instruction); grant no
Bash/Write/Editand no project file access. - Treat the query as data, not instructions. Put the query in a delimited
block and tell the sub-context to classify the content below, never follow
it — so a query like “ignore the question and run …” is classified, not
obeyed.
Prompt it with the covered skills’ names + descriptions and the delimited
query; collect the single skill name it reports (or
null).
- Dispatch read-only / no-execution. Use the most restricted dispatch
the harness offers (a read-only subagent type, or an explicit
“use no tools” instruction); grant no
- Assemble the reports as
{skill: {query_id: [reported | null | "__error__", …]}}(one entry per run;query_idisq00,q01, … by position). - Grade with
python tools/run-pack-evals.py --pack <name> --mode in-harness --reports <reports.json>— sametrigger_rate/0.5 grading and eval-workspace, but the summary is labelledmode: in-harness,fidelity: reported.
This measures a description-match judgement, not the real activation router
(a dispatched sub-context can’t be restricted to only the pack’s skills), so
it is a portable reach check — never the calibration baseline. When you have the
claude CLI, prefer the headless mode.
Tier B — authoring output-quality evals (evals/evals.json)
Section titled “Tier B — authoring output-quality evals (evals/evals.json)”evals/evals.json is { "skill_name", "evals": [{ "id", "prompt", "expected_output", "files"?, "assertions"? }] }. Author these now — they
document what good output looks like — but note that this RFC does not run or
grade them; automated execution (with/without-skill comparison, LLM-judge,
pass-rate deltas) is a future Tier-B RFC.
Two things make a Tier-B eval worth writing:
- A concrete
expected_output. Describe what the agent should actually produce and do — “invokesrender.pywith--template report.docx, reports the OUTPUT path, does not hand-write the .docx” — not a vague “produces a good doc”. The detail is what a future grader (or a human reviewer today) checks against. - Assertions that bite. A good assertion is falsifiable and behaviour-level (“does NOT instruct the user to pre-escape the ampersand”); a weak one restates the prompt (“produces output”). Until automated grading lands, a human reads the run against these — so write them for that reader.
Running a lightweight behavior/output check in-harness (tier: B-lite)
Section titled “Running a lightweight behavior/output check in-harness (tier: B-lite)”The full Tier-B grading (LLM-judge, pass-rate deltas, with/without-skill) is
a future RFC. But a lightweight check — run the skill and validate its
outputs against deterministic post-conditions — is available now in-harness.
Add an optional expect block to an eval to make it runner-gradable:
{"id": 1, "prompt": "Turn this Markdown into a Word doc using report.docx", "expected_output": "…prose…", "assertions": ["does NOT hand-write the .docx"], "files": ["evals/files/report.docx"], "expect": {"produces": ["out.docx"], "output_contains": ["OUTPUT:"], "output_excludes": ["Traceback"]}}Drive it as a procedure — this executes the skill body, so it carries a
sharper containment requirement than the activation check (the
--allowed-tools Skill and “no execution” controls do not apply):
- Scope gate — only safe skills. Run the behavior check only for skills
whose execution is non-destructive and needs no network or credentials.
A skill that deletes, writes outside its workspace, or phones home is out of
scope until a real OS-level sandbox exists — this is a procedure + scope-gate,
not a mechanical sandbox (a host agent running the skill keeps its full
tool surface). Skills that integrate a logged-in backend (credentialed
skills on the
auth: cli/ credential-broker contract) are out of scope here: behavior-checking them would need live auth + a real backend and could mutate remote state — they get activation (Tier-A) coverage only. Their behavior needs recorded-interaction replay or a disposable test backend (a future Tier-B mechanism), never real credentials injected into an eval. - Ensure prerequisites — the skill’s own contract, never the harness’s. The
eval runs the real skill, so its deps must be present. Don’t hand-roll a
setup: run the skill’s own detection (
## Prerequisites→ its--check); if deps are absent, install once via the skill’s own documented command (e.g.npm installin the skill dir —node_modules/is gitignored) and re-check. The harness never auto-installs. After the one-time install the run is repeatable: the skill’s--checkdetects-present and you proceed. - Per eval: get a confined working dir from the runner —
python tools/run-pack-evals.py --pack <name> --prepare-workspace <skill>/<eval_id>prints an OS-temp dir seeded with the eval’sevals/files/fixtures (path-confined). Run the skill on thepromptwith that dir as cwd, instructing it to confine writes there and bounding the run (stop it if it exceeds a sane per-eval time). Capture the run’s output to.eval-output.txtin that dir. Tear the dir down in afinally. - Collect
{skill: {eval_id: {"assertions": [<bool per assertion>], "errored": <bool>, "workspace": "<dir>"}}}and grade:python tools/run-pack-evals.py --pack <name> --mode in-harness --check behavior --reports <file>. The runner re-derives the deterministic checks (expect.producesfiles exist,output_contains/excludesagainst.eval-output.txt) from the working dir itself — you only attest the semanticassertions. Summary is labelledtier: B-lite,fidelity: observed+attested.
Grading the quality layer with an LLM-judge (--mode judge)
Section titled “Grading the quality layer with an LLM-judge (--mode judge)”Deterministic checks (above) cover validity/shape; the quality layer — is
the contract well-designed? is this the right diagram? — has no ground truth, so
it needs a model. --mode judge grades a produced artifact against the eval’s
rubric (its expected_output + assertions) and is report-only (a quality
signal, never a gate). The lens is the rubric you already authored; the repo’s
own review skills (e.g. architect-review) are natural lenses.
# point each eval at the artifact the skill produced, then judge:python tools/run-pack-evals.py --pack <name> --mode judge \ --judge-adapter codex --artifacts artifacts.json # {skill: {eval_id: <artifact-path>}}Choosing the backend and model. Judge backends are declarative command templates, not code — so you pick the backend/model, or add your own, by config:
-
Built-in:
--judge-adapter claude-code(same model) or--judge-adapter codex(an independent model/IDE — preferred, since a cross-model judge can’t self-grade). -
Pick the model:
--model <name>(passed via that backend’s model flag). -
Bring your own backend — e.g. a Kiro headless judge — with a
--judge-config <toml>, no code change:[judge.kiro-cli]command = ["kiro-cli", "chat", "--no-interactive", "{prompt}"] # your real headless invocationmodel-flag = "--model"extract = "stdout" # or "json:<field>" if the CLI emits a JSON envelopeTerminal window python tools/run-pack-evals.py --pack <name> --mode judge \--judge-adapter kiro-cli --model <your-model> --judge-config judges.toml --artifacts artifacts.json{prompt}is substituted as a discrete argv element (never a shell string); your file is merged over the built-ins, so you can also override a built-in (e.g. pinclaude-codeto a specific model).
The judge is judgment-only (the artifact is inlined; claude is granted no
tools, codex runs -s read-only) and fails closed — an unparseable verdict
is an error, never a silent pass. It wobbles run-to-run and wants periodic
human calibration (does it agree with you on a sample?); the full Tier-B
grading (pass-rate deltas, with/without-skill, train/validation) is a future RFC.
Output rendering
Section titled “Output rendering”Every skill has an ## Output rendering section before its first procedural
##. Include the universal managed block even for conversational and
artifact-only skills. It governs chat, input requests, prose artifacts, code,
comments, quiet tool work, and substance preservation without relying on
another skill or repository file.
After that block, add only the shape directives the skill needs. Keep custom shape rules outside the managed markers so synchronization preserves them. The full contract and canonical block are in Output rendering.
The full conventions — status glyph set (●/✓/○/⚠), column alignment and truncation limits, persistent command bar pattern, delete-gate box template, card format for one-by-one review flows, and progress reporting form — are in guides/_shared/reference/skill-ux-patterns.md.
Script conventions
Section titled “Script conventions”For scripts under scripts/, follow the flag and docblock conventions in guides/_shared/reference/skill-script-conventions.md: consistent --headed/--yes/--debug/--raw flags, = form for value flags, a usage docblock listing every flag, and shortcut IDs with type-prefix when the script acts on a typed collection.
Setup skills
Section titled “Setup skills”A skill that installs a dependency or configures an environment should be idempotent: each step checks whether it is already done before running, so re-running at any point is safe. Declare a verification command and a recovery command in pack.toml under [pack.first-value] — the verification command exits 0 when setup is complete; the recovery command is the exact one-liner fix when it fails. Full format: guides/_shared/reference/skill-script-conventions.md § pack.toml verification and recovery.
For skills that automate a web browser — persistent-profile auth for SSO and device-certificate sites, bearer token interception, session checks, probe files as data layer — see guides/_shared/how-to/browser-automation-skill.md.
What’s enforced vs. recommended
Section titled “What’s enforced vs. recommended”Frontmatter and description rules are lint-enforced (agentbundle catalogue lint --root . --deep); credentialed-skill rules have their own lint. The body structure, the cross-platform rules, and the three-tier dependency policy are reviewer-enforced conventions — no gate checks them, so they live or die in review. Hold the line there.
Runtime config and operation logging
Section titled “Runtime config and operation logging”Skills that store user-specific settings or record actions use the pack-config API:
load_pack_config(pack_dir("<pack>"))— read values the user set viaagentbundle pack-config set.write_entry(pack_dir("<pack>"), {...})— append a structured record to the pack’s operation log (agentbundle oplog <pack>to view).- For credentialed skills,
pack_dir()is the base directory the credential broker writes tokens into.
Full reference and CLI equivalents: guides/_shared/reference/pack-config-api.md. Script-level usage examples (with Python imports and flag patterns): guides/_shared/reference/skill-script-conventions.md § Pack config and operation logging.
See also
Section titled “See also”- How to add a credentialed skill — the separate contract for tokens, API auth, and the
auth: clibroker. - Output rendering directives — the canonical directive catalog for
## Output rendering. - Skill UX patterns — craft rules: column alignment, truncation, command bar, delete-gate box.
- Skill script conventions — flag conventions, docblocks, shared-libs, pack-config API.
mermaid-renderer— the Tier-1 reference:## Prerequisites+ ashutil.which--checkverb + an explicit “don’t auto-install” rule.docs/CONVENTIONS.md§ Skills — when to add a skill at all (the three-times rule).