A skill is a markdown file that Claude Code executes — there’s no engine you must integrate with, so writing one is mostly writing good instructions. The bar upstream holds skills to is docs/skill-guidelines.md, and it boils down to two principles: minimal integration surface (mostly add files; keep reach-ins into existing code to a line or two that calls your own code) and a test for every functional integration point (one that goes red if the wiring is deleted or drifts). This page covers the conventions; the overview explains the model.
Don’t start from a blank file. The /learn skill distills a SKILL.md from a source — a directory, a URL, pasted notes, or the work you just did in a conversation — authored to these same guidelines, and refines an existing skill the same way. Generate the first draft, then tighten it by hand against the conventions below.
Anatomy of a SKILL.md
Every skill is a directory under .claude/skills/<name>/ with a SKILL.md in the Claude Code skills format:
name — lowercase, alphanumeric plus hyphens, max 64 characters. This becomes the slash command (/my-skill).
description — required, and load-bearing: it’s how Claude decides whether to invoke the skill. Write what it does and when to use it. manage-mounts even lists its trigger phrases: Triggers on "mounts", "mount allowlist", "agent access to directories", "container mounts".
The body is prose an agent can run. Real skills converge on the same shape: numbered apply steps, then a build-and-validate step at the end. Keep it under 500 lines — move detail to separate reference files in the skill directory — and put code in separate files, never inline in the markdown.
Conventions that make a skill safe to re-run
Apply must be idempotent: upgrades re-run it (/update-skills works by re-running each installed skill’s own apply), and a skill that half-applies twice is a bug. Three conventions from the in-tree skills:
Every step guards itself. Each step states its own skip condition — “skipped if the line is already present”, “overwrite — the branch is canonical” — so a partial apply completes instead of duplicating lines. The trunk channel and provider skills (the ones the setup wizard drives) go further: each mechanical step also carries an nc:<kind> directive fence (copy, append, dep, json-merge, plus run, prompt, operator, env-set) that a deterministic engine can apply from the same document an agent reads. Directives are idempotent by construction (copy overwrites, append skips-if-present, env-set sets-if-absent), and anything the engine can’t apply falls back to the prose beside the fence — so the prose must stand on its own. For a contributed skill the fences are optional: you’re held to the checklist on this page; opting in brings the conformance suite and the directive lint (pnpm exec tsx scripts/skill-directives.ts .claude/skills/<name>/SKILL.md) with it. Grammar and semantics: docs/skill-directives.md.
Pinned versions. Dependencies install at an exact version — add-telegram pins @chat-adapter/telegram@<exact-version> in its nc:dep fence; the supply-chain policy rejects ranges and latest. Same for Dockerfile-installed binaries: pin with an ARG <X>_VERSION= line.
Present-tense DO steps only. A skill is a standalone artifact with no memory of its own edits. No “earlier versions did X”, no “no changes needed here” — those are flagged anti-patterns. So is a separate VERIFY.md: tests are the verification, and the final step is always pnpm run build plus running the skill’s own tests.
REMOVE.md
REMOVE.md is required exactly when apply leaves anything behind. A pure instruction-only skill that copies nothing needs none. When it’s required, it must reverse every change apply made — the most common review rejection is an incomplete one. add-telegram’s REMOVE.md is the template; it mirrors the apply step for step:
- Delete the barrel import line from
src/channels/index.ts (delete, never comment out), then rm every copied file — adapter, helpers, setup step, and tests
- Delete the
STEPS map entry from setup/index.ts
- Remove the skill’s env vars from
.env and re-sync to the container
pnpm uninstall the dependency
- Rebuild and restart the service
Each removal step is itself idempotent (“skip if already gone”).
Testing your skill
Every reach-in with a functional consequence gets a test that goes red if the wiring is deleted or drifts. Tests targeting host code use vitest; container code uses bun:test under container/agent-runner/. Tests travel with the skill — they ship in the skill’s directory (or on the registry branch) and apply copies them into the project tree, so they run against the composed system via pnpm test. There’s also a dedicated config, vitest.skills.config.ts, that picks up tests under .claude/skills/**/tests/ — run it with pnpm exec vitest run --config vitest.skills.config.ts.
The most common shape is the registration test: import the real barrel, assert the registry contains your entry. add-telegram’s telegram-registration.test.ts imports src/channels/index.ts and asserts telegram is registered — it goes red if the import line is deleted, if the barrel fails to evaluate, or if the adapter package isn’t installed (the unmocked import throws), so it guards the dependency for free. Don’t test your function directly instead: calling your own code bypasses the wiring and stays green when the barrel line is deleted.
When the edit lives somewhere you can’t invoke (a main() call, a container boot file), fall back to a structural test. add-ollama-tool ships one that parses the container’s index.ts with the TypeScript compiler API and asserts the mcpServers object literal has an ollama entry pointing at the right module.
Two rules apply across all of them: mock only genuinely external services (a fake HTTP server, stubbed creds), never the package you’re guarding; and pnpm run build is an always-on leg, because typed drift slips past runtime tests. Finally, the robustness check from the guidelines: apply your skill with a small, cheap model. If it fumbles the instructions, the instructions are too vague — fix them, don’t blame the model.
A minimal template
A template for a small operational skill — instruction-only, so no REMOVE.md and no integration tests, just idempotent steps. Adapt the phases to your task:
Private skill or upstream contribution?
For your own install, just create the directory: drop .claude/skills/<name>/SKILL.md into your checkout and Claude Code picks it up as /<name> — no registration, no build step. This is the right home for anything specific to your setup, and where every skill should start.
To contribute upstream, your skill needs to be useful beyond your own setup and meet the guidelines bar above. Upstream recognizes four types — channel/provider skills (code on the registry branches), utility skills (code files in the skill directory), operational skills (instructions only), and container skills (container/skills/, loaded by the agent inside the container) — each with its own PR path. See Contributing for the flow, and note that for channel and provider skills you PR against main and maintainers land the code on the registry branch from your work.
Skills are not sandboxed. Unlike agent containers, a skill runs in your Claude Code session on the host, with whatever permissions that session has — read untrusted skills before running them.
Related pages