You wrote a skill. It works on your machine. Now a teammate asks for it and you're tempted to paste a wall of markdown into Slack. Don't. The point of packaging a skill in Claude Code is that they run two commands and get the exact thing you have — description, triggers, scripts, and all.
This is the path from a local folder to something installable, in about fifteen minutes. Every command below is copy-paste-ready.
Step 1: Confirm the folder structure
A skill is a folder with one required file: SKILL.md. The directory name becomes the slash command, so summarize-changes/ gives you /summarize-changes.
my-skill/
├── SKILL.md # required — YAML frontmatter + markdown body
├── reference.md # optional — loaded only when needed
└── scripts/helper.py # optional — executed, not loaded
Keep SKILL.md under 500 lines. Bulky reference material goes in sibling files so it costs zero tokens until something loads it. This isn't a style preference — it's a runtime one. An invoked skill's body stays in context for the whole session, and auto-compaction keeps only the first 5,000 tokens per skill inside a 25,000-token combined budget. A fat body crowds out everything else.
Step 2: Write the description that controls invocation
description is the one field that decides when your skill fires. It's also the only field worth obsessing over — name is optional and, for a skill in a directory, defaults to the directory name anyway. Claude reads description to decide whether to auto-load the skill, so this is where you win or lose.
The canonical pattern: say what it does, then list triggers with Use when…
---
description: Summarizes uncommitted changes and flags anything risky. Use when the user asks what changed, wants a commit message, or asks to review their diff.
---
Two rules to internalize:
- Put the key use case first. The combined
descriptiontext (plus the optionalwhen_to_usefield) is truncated at 1,536 characters in the skill listing. Front-load it. - Tune by symptom. Fires too often → make the description more specific. Doesn't fire → add the keywords users actually type.
Step 3: Gate who can invoke it
Two frontmatter fields decide whether the human, Claude, or both can run a skill:
disable-model-invocation: true→ only the human can run it. Use this for side-effect workflows like/deployor/commit. Bonus: it keeps the description out of context entirely until you invoke it.user-invocable: false→ only Claude can run it (hidden from the/menu). Use this for background knowledge that isn't a user-facing action.- Neither field → both can invoke. This is the default.
If your skill does something irreversible, set disable-model-invocation: true and sleep better.
Step 4: Scope it to the right place
Scoping isn't a config flag — it's location. Where the folder lives decides who gets it.
| Location | Path | Applies to |
|---|---|---|
| Personal | ~/.claude/skills/<name>/SKILL.md | All your projects |
| Project | .claude/skills/<name>/SKILL.md | This repo (commit it to share) |
| Plugin | <plugin>/skills/<name>/SKILL.md | Wherever the plugin is enabled |
Precedence runs enterprise > personal > project. In a monorepo, a nested .claude/skills/ under a subdirectory — say apps/web/.claude/skills/deploy/ — auto-loads only when Claude touches files there, and if the name clashes with another skill it shows up under a directory-qualified name like /apps/web:deploy.
One quality-of-life note: edits live-reload. Adding, editing, or removing a skill under ~/.claude/skills/, the project .claude/skills/, or an --add-dir directory takes effect inside the current session. Only creating a brand-new top-level skills directory that didn't exist at startup needs a restart.
Step 5: Version it on purpose
Versioning lives in plugin.json, and the behavior is counterintuitive enough to bite you:
- Set
version→ users get updates only when you bump it. Most teams use explicit semver for exactly this control. - Omit
version(git-hosted) → the commit SHA is used and every commit counts as a new version. Fine for fast internal iteration, alarming in production.
Pick deliberately. "It auto-updated" is rarely the story you want to be debugging at 2 a.m.
Step 6: The publish-and-install dry run
Here's the payoff. A skill ships as a plugin inside a marketplace. Minimal layout:
my-marketplace/.claude-plugin/marketplace.json
my-marketplace/plugins/quality-review-plugin/.claude-plugin/plugin.json
my-marketplace/plugins/quality-review-plugin/skills/quality-review/SKILL.md
plugin.json:
{ "name": "quality-review-plugin", "description": "...", "version": "1.0.0" }
marketplace.json:
{
"name": "my-plugins",
"owner": { "name": "Your Name" },
"plugins": [
{
"name": "quality-review-plugin",
"source": "./plugins/quality-review-plugin",
"description": "..."
}
]
}
Install locally first — this is the real dry run, and it catches path mistakes before anyone else sees them:
/plugin marketplace add ./my-marketplace
/plugin install quality-review-plugin@my-plugins
/quality-review-plugin:quality-review
Note the last line: plugin skills are always namespaced as plugin-name:skill-name, never bare. That namespacing is exactly what prevents two plugins with a review skill from colliding. Once it works locally, push the marketplace to GitHub or GitLab and hand teammates the same two commands with owner/repo instead of the local path:
/plugin marketplace add owner/repo
/plugin install quality-review-plugin@my-plugins
They refresh with /plugin marketplace update. Pick a distinct kebab-case name for your marketplace so it can't be confused with Anthropic's own public marketplaces, claude-plugins-official (curated) and claude-community (the reviewed community catalog).
Step 7: Sanity-check before you publish
Don't ship a description you tested by vibes. The two failure modes are mechanical, so test for them directly: open a session, ask the things a user would actually ask, and watch whether the skill fires. Then ask things it should not touch and confirm it stays quiet. Fires too eagerly → tighten the description. Stays silent → add the words users really type.
Run /doctor afterward to confirm none of your skill descriptions are being shortened or dropped from context — a description that gets truncated past its key use case is a description that won't reliably fire.
Two sidebars worth your time
It's portable. The SKILL.md format — name plus description plus markdown body — follows the Agent Skills open standard published at agentskills.io, and Claude Code is one of a long list of harnesses that read it, alongside OpenAI Codex CLI, Cursor, Gemini CLI, GitHub Copilot, and more. Claude-Code-specific frontmatter like invocation control or allowed-tools is simply ignored by tools that don't support it, so nothing breaks. One skill, many harnesses.
Pre-approve tools to kill prompts. allowed-tools grants permission for the listed tools while the skill is active, so it runs without permission interruptions:
allowed-tools: Bash(git add *) Bash(git commit *)
It doesn't restrict anything — every other tool stays callable and your permission settings still apply. For project skills this takes effect only after the user accepts the workspace-trust dialog — which is the right moment to remind everyone: review a project skill before you trust a repo. A skill can grant itself broad tool access, and "I just installed it" is not a security model.
What to remember
Three things separate a skill that travels from one that gets pasted into chat and forgotten. description controls invocation — write it for the words users actually say. Location controls scope — drop the folder where the right people will find it. version controls updates — set it on purpose, because the default (every commit is a release) surprises people in production. Build the folder, write the description, gate it, scope it, version it, run the local install dry run, sanity-check it. Fifteen minutes, and the next teammate who asks gets two commands instead of a wall of markdown.
