A Claude Code skill is a SKILL.md file plus optional supporting files that teach Claude a repeatable procedure. Kept in ~/.claude/skills/ or a project’s .claude/skills/, a skill is personal or project-local. To hand one to a teammate, ship it across every project, or publish it to the community, you wrap it in a plugin and list that plugin in a marketplace that others can add with one command.

This tutorial builds a real skill, packages it as a plugin, catalogs it in a marketplace, validates the manifests with the claude CLI, tests it locally, and distributes it through GitHub. The end result is a repository anyone can install with /plugin marketplace add your-org/your-repo followed by /plugin install.

What you will build

A plugin named changelog-helper that adds one skill, /changelog-helper:changelog. The skill reads the commits since your last release tag, then asks Claude to draft a grouped changelog entry. It demonstrates the three features that make a skill worth packaging: a bundled helper script, dynamic context injection, and per-skill tool permissions.

The finished repository doubles as a marketplace, so the same git repo both defines the plugin and distributes it:

changelog-marketplace/
├── .claude-plugin/
│   └── marketplace.json                # Catalog: lists the plugin and its source
├── plugins/
│   └── changelog-helper/
│       ├── .claude-plugin/
│       │   └── plugin.json             # Plugin manifest: name, version, metadata
│       └── skills/
│           └── changelog/
│               ├── SKILL.md            # The skill itself
│               └── scripts/
│                   └── commits-since-tag.sh
├── Makefile                            # validate / pack / test targets
└── .gitignore

Prerequisites

  • macOS (the commands are macOS-first; they work on Linux unchanged).
  • Claude Code v2.1.128 or later. Namespaced plugin skills, claude plugin validate, and zip-based --plugin-dir all need a recent build. This tutorial was validated on v2.1.207. Check yours with claude --version; upgrade with claude update if needed.
  • git — the marketplace is distributed as a git repository, and the skill’s helper script shells out to git.
  • make and zip — preinstalled on macOS; used by the Makefile.
  • A GitHub account, for the final distribution step.

No language runtime is required. The plugin is Markdown, JSON, and one POSIX shell script, so consumers install it with zero dependencies.

How the three layers relate

Skills, plugins, and marketplaces are nested, not competing:

  • A skill is the unit of behavior: one SKILL.md and its supporting files.
  • A plugin is the unit of distribution: a directory with a .claude-plugin/plugin.json manifest that can bundle skills, agents, hooks, and MCP servers. Plugin skills are namespaced as /plugin-name:skill-name, so they never collide with a user’s own skills.
  • A marketplace is the unit of discovery: a .claude-plugin/marketplace.json catalog that points at one or more plugins. Users add the marketplace once, then install any plugin it lists.

You can keep a skill standalone in .claude/skills/ while you iterate, then promote it to a plugin when it is ready to share. This tutorial starts at the plugin layer directly, because the goal is distribution.


Step 1: Scaffold the repository and ignore build artifacts

Create the file

Create the directory tree and the .gitignore before anything else, so generated archives never land in a commit.

mkdir -p changelog-marketplace/.claude-plugin
mkdir -p changelog-marketplace/plugins/changelog-helper/.claude-plugin
mkdir -p changelog-marketplace/plugins/changelog-helper/skills/changelog/scripts
cd changelog-marketplace
touch .gitignore

Add the code: changelog-marketplace/.gitignore

# Build artifacts produced by `make pack`
dist/
*.zip

# macOS clutter
.DS_Store

# Local Claude Code state, if you test inside this repo
.claude/

Detailed breakdown

  • .claude-plugin/ at two levels. The one at the repo root holds marketplace.json (the catalog). The one inside plugins/changelog-helper/ holds plugin.json (the manifest). Only these manifest files go inside a .claude-plugin/ directory. Every other directory (skills/, agents/, hooks/) lives at the plugin root, not inside .claude-plugin/. Nesting them wrong is the single most common plugin bug.
  • plugins/ subdirectory. Putting the plugin under plugins/ keeps the repo tidy and lets the marketplace grow to several plugins later. The marketplace catalog will point at it with a relative path.
  • .gitignore first. The dist/ folder and *.zip files are created by the pack target in Step 6. Ignoring them up front keeps the repository to source files only. .claude/ is ignored in case you run Claude Code from inside this repo while testing, so your local session state is never committed.

Step 2: Write the skill

Create the file

touch plugins/changelog-helper/skills/changelog/SKILL.md

Add the code: plugins/changelog-helper/skills/changelog/SKILL.md

---
description: Draft a grouped changelog entry from the commits since the last release tag. Use when preparing release notes, writing a CHANGELOG, or summarizing what shipped.
argument-hint: [version]
allowed-tools: Bash(git tag *), Bash(git log *)
---

# Changelog entry

## Commits since the last release

```!
bash ${CLAUDE_SKILL_DIR}/scripts/commits-since-tag.sh
```

## Your task

Draft a changelog entry for version **$ARGUMENTS** from the commits above.

1. Group the commits under `### Added`, `### Changed`, `### Fixed`, and
   `### Removed`. Omit any group that has no entries.
2. Rewrite each commit subject as a user-facing sentence. Drop noise like
   "wip", "fixup", and merge commits.
3. Start the entry with a `## $ARGUMENTS` heading. If `$ARGUMENTS` is empty, use
   `## Unreleased`.
4. Output only the Markdown entry, ready to paste at the top of `CHANGELOG.md`.

Detailed breakdown

  • Frontmatter drives discovery. The description is the only text Claude sees when deciding whether to load the skill automatically, so it leads with the action and lists trigger phrases (“release notes”, “CHANGELOG”). Put the key use case first: the listing truncates long descriptions.
  • argument-hint: [version] shows [version] in the autocomplete menu, so a user knows to type /changelog-helper:changelog v1.4.0.
  • allowed-tools pre-approves exactly the two git read commands the skill needs, so Claude runs them without a permission prompt each time. It grants access; it does not restrict anything else. Scope it narrowly rather than granting Bash(*).
  • Dynamic context injection. The ```! fenced block runs before Claude sees the skill. Claude Code executes the script and replaces the block with its output, so the prompt arrives with the actual commit list already inlined. ${CLAUDE_SKILL_DIR} resolves to this skill’s directory no matter where the plugin is installed, which is what makes the bundled-script reference portable.
  • $ARGUMENTS expands to whatever the user types after the skill name. The body handles the empty case explicitly so the skill still produces valid output when invoked with no version.
  • Note the outer fence. In this listing the file is shown inside a four-backtick block so its own triple-backtick ```! block renders. In the actual SKILL.md, the ```! block uses three backticks.

Step 3: Add the bundled helper script

Create the file

touch plugins/changelog-helper/skills/changelog/scripts/commits-since-tag.sh
chmod +x plugins/changelog-helper/skills/changelog/scripts/commits-since-tag.sh

Add the code: plugins/changelog-helper/skills/changelog/scripts/commits-since-tag.sh

#!/usr/bin/env bash
# Print the commit subjects since the most recent annotated or lightweight tag.
# Falls back to the entire history when the repository has no tags yet.
set -euo pipefail

if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
  echo "(not inside a git repository)"
  exit 0
fi

if ! git rev-parse --verify --quiet HEAD >/dev/null 2>&1; then
  echo "(no commits yet)"
  exit 0
fi

last_tag="$(git describe --tags --abbrev=0 2>/dev/null || true)"

if [ -n "$last_tag" ]; then
  echo "Commits since ${last_tag}:"
  range="${last_tag}..HEAD"
else
  echo "No tags found. Showing all commits:"
  range="HEAD"
fi

count="$(git log --oneline "$range" | wc -l | tr -d ' ')"
if [ "$count" = "0" ]; then
  echo "(no commits in range)"
  exit 0
fi

git log --no-merges --pretty=format:'- %s' "$range"
echo

Detailed breakdown

  • Why a bundled script. A skill can inline a git command directly, but real logic (tag detection, an empty-range guard, a no-tags fallback) is clearer and testable as a script. Bundling it means the plugin carries its own tooling and works the same on every machine that installs it.
  • set -euo pipefail makes the script fail loudly on an error, an unset variable, or a broken pipe, rather than emitting partial output that Claude would treat as real data.
  • The two git rev-parse guards keep the script from erroring in the two states where git commands would abort: outside a repository, and inside a freshly initialized repo that has no commits yet (where HEAD does not resolve). Each prints a readable line and exits 0. The dynamic-context block inlines whatever the script prints, so a clean message beats a stack trace.
  • git describe --tags --abbrev=0 finds the most recent tag reachable from HEAD. The || true prevents set -e from aborting when there are no tags, and the branch below switches the range to full history.
  • --no-merges drops merge commits so the changelog input is just the real work. --pretty=format:'- %s' prints each subject as a Markdown bullet, which is already close to the shape Claude turns into the final entry.
  • Executable bit. chmod +x is a convenience; the SKILL.md invokes the script with an explicit bash ..., so it runs even if the bit is lost in transit (some zip and copy operations drop it).

Step 4: Write the plugin manifest

Create the file

touch plugins/changelog-helper/.claude-plugin/plugin.json

Add the code: plugins/changelog-helper/.claude-plugin/plugin.json

{
  "name": "changelog-helper",
  "description": "Draft a grouped changelog entry from commits since the last release tag.",
  "version": "1.0.0",
  "author": {
    "name": "Your Name"
  },
  "homepage": "https://github.com/your-org/changelog-marketplace",
  "license": "MIT"
}

Detailed breakdown

  • name is the namespace. Every skill in this plugin is invoked as /changelog-helper:<skill>, and name supplies the changelog-helper prefix. It must be kebab-case with no spaces.
  • version controls updates. When version is set, users only receive an update when you bump this field. If you omit it and distribute via git, every commit counts as a new version. Setting an explicit semver string gives you a deliberate release cadence, so bump it on each release.
  • author, homepage, license are optional metadata shown in the plugin manager and helpful for attribution. The other directories the manifest can describe (skills/, agents/, hooks/, .mcp.json) are discovered by convention from the plugin root, so a skills-only plugin needs no extra fields.
  • What is not here. The manifest does not list the skill. Claude Code finds skills/changelog/SKILL.md automatically because it sits at the plugin root under skills/. You only add explicit component paths when you deviate from the default layout.

Step 5: Write the marketplace catalog

Create the file

touch .claude-plugin/marketplace.json

Add the code: changelog-marketplace/.claude-plugin/marketplace.json

{
  "name": "changelog-tools",
  "owner": {
    "name": "Your Name",
    "email": "[email protected]"
  },
  "metadata": {
    "description": "Release and changelog helpers for Claude Code."
  },
  "plugins": [
    {
      "name": "changelog-helper",
      "source": "./plugins/changelog-helper",
      "description": "Draft a grouped changelog entry from commits since the last release tag.",
      "version": "1.0.0",
      "category": "release",
      "tags": ["changelog", "release", "git"]
    }
  ]
}

Detailed breakdown

  • name is what users type. Installing this plugin is /plugin install changelog-helper@changelog-tools — plugin name @ marketplace name. Each user registers only one marketplace per name, so pick something specific and stable. A set of reserved names (anything resembling an official Anthropic marketplace) is blocked.
  • source: "./plugins/changelog-helper" is a relative path from the marketplace root (the directory containing .claude-plugin/), not from the .claude-plugin/ folder. It must start with ./. Relative sources resolve when users add the marketplace from git or a local path. For other hosting, a plugin entry’s source can instead be a github, url, git-subdir, or npm object, letting one marketplace list plugins that live in other repositories.
  • The duplicated version. Listing version both here and in plugin.json is intentional. The validator warns if they disagree, which catches the classic release mistake of bumping one and forgetting the other.
  • category and tags power discovery and search in the plugin manager. They are optional but cheap to add.
  • owner is required. name is mandatory; email is optional but gives users a contact for a plugin they are about to trust with tool access.

Step 6: Add a Makefile for validation and packaging

Create the file

touch Makefile

Add the code: changelog-marketplace/Makefile

# Changelog marketplace — validation and packaging tasks.
PLUGIN_DIR := plugins/changelog-helper
PLUGIN_NAME := changelog-helper
DIST := dist

.DEFAULT_GOAL := help

.PHONY: help validate validate-plugin test-script pack clean

help: ## Show this help screen
	@echo "Changelog marketplace tasks:"
	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
		| awk 'BEGIN {FS = ":.*?## "} {printf "  \033[36m%-16s\033[0m %s\n", $$1, $$2}'

validate: ## Validate the marketplace catalog (strict)
	claude plugin validate . --strict

validate-plugin: ## Validate the plugin manifest and its skill files (strict)
	claude plugin validate ./$(PLUGIN_DIR) --strict

test-script: ## Run the bundled helper script against this repo
	bash $(PLUGIN_DIR)/skills/changelog/scripts/commits-since-tag.sh

pack: ## Build a distributable zip of the plugin for --plugin-dir / --plugin-url
	@mkdir -p $(DIST)
	@rm -f $(DIST)/$(PLUGIN_NAME).zip
	cd $(PLUGIN_DIR) && zip -r ../../$(DIST)/$(PLUGIN_NAME).zip . -x '*.DS_Store'
	@echo "Wrote $(DIST)/$(PLUGIN_NAME).zip"

clean: ## Remove build artifacts
	rm -rf $(DIST)

Detailed breakdown

  • Default target prints help. .DEFAULT_GOAL := help makes a bare make print the target list instead of running the first rule. The grep/awk pipeline reads the ## comment after each target name, so the help screen stays in sync with the targets automatically as you add more.
  • validate / validate-plugin. Pointed at the repo root, the validator checks marketplace.json for schema errors, duplicate plugin names, path traversal in sources, and a version mismatch against each local plugin’s plugin.json. Pointed at the plugin directory, it also checks the plugin.json and the frontmatter of every skill, agent, command, and hook. --strict turns warnings (unrecognized fields, missing metadata) into a non-zero exit, which is what you want in CI.
  • test-script runs the bundled script directly, independent of Claude, so you can confirm it produces sane output before trusting it inside the skill.
  • pack zips the plugin’s contents (not the wrapping folder) so the archive’s root holds .claude-plugin/plugin.json. That archive works with claude --plugin-dir ./changelog-helper.zip (v2.1.128+) and with --plugin-url when hosted at a URL. The dist/ output is gitignored from Step 1.
  • Tabs, not spaces. Recipe lines must be indented with a real tab, the usual Makefile gotcha. Copy the block verbatim.

Step 7: Validate the manifests

Run the validator before you ever try to load the plugin. It catches malformed JSON, misplaced directories, and frontmatter errors that would otherwise surface as a confusing “skill not found” later.

make validate
make validate-plugin

Both targets print ✔ Validation passed and exit 0 when the manifests are clean. Because both pass --strict, any unrecognized field or missing recommended metadata fails the build, so a green run here means the manifests are genuinely correct, not merely parseable. (To see the components Claude Code detects in the plugin, along with its token cost, use claude plugin details changelog-helper after installing it in Step 9.)

If validation fails, the message names the file and the problem (for example, plugins[0] plugin.json → missing required field "name"). Fix the named file and re-run. Confirm the helper script works on its own, too:

make test-script

Its output depends on where you run it. Run from inside a git project with history and no tags, it prints No tags found. Showing all commits: followed by one - <subject> bullet per non-merge commit. In a tagged repository it prints Commits since <tag>: and only the commits after that tag. In a freshly created folder that is not yet a git repository (before Step 10’s git init) it prints (not inside a git repository), and in a repo with no commits yet it prints (no commits yet). The exact subjects depend on your history.


Step 8: Test the plugin locally with --plugin-dir

The fastest development loop skips installation entirely. Launch Claude Code with the plugin loaded directly from disk:

claude --plugin-dir ./plugins/changelog-helper

Inside that session, invoke the skill by its namespaced name:

/changelog-helper:changelog v1.0.0

Claude Code runs the bundled script through the dynamic-context block, hands the commit list to Claude, and Claude returns a drafted ## v1.0.0 changelog entry. Run /help to confirm the skill is listed under the plugin namespace.

As you edit SKILL.md, run /reload-plugins in the session to pick up changes without restarting. To test the packaged archive instead of the directory, run make pack and load the zip:

claude --plugin-dir ./dist/changelog-helper.zip

Step 9: Install from the local marketplace

--plugin-dir loads a single plugin. To exercise the full install path the way your users will, add the marketplace and install from it. Point the marketplace add at the repo root (the directory above .claude-plugin/):

claude plugin marketplace add ./
claude plugin install changelog-helper@changelog-tools

claude plugin marketplace add accepts a local path, a git URL, or a GitHub owner/repo shorthand. The equivalent in-session commands are /plugin marketplace add and /plugin install. Verify the install:

claude plugin list

The changelog-helper plugin appears, sourced from the changelog-tools marketplace. Its skill is now available as /changelog-helper:changelog in any project, not just this repo.

To remove the local test install before publishing for real:

claude plugin marketplace remove changelog-tools

Removing a marketplace from its last scope also uninstalls the plugins you installed from it, so this cleans up in one step.


Step 10: Distribute through GitHub

Publishing is just pushing the repository. The marketplace.json at the root is what turns a plain repo into an installable marketplace.

Create the file

git init
git add .
git commit -m "changelog-helper plugin and marketplace v1.0.0"
git branch -M main
git remote add origin [email protected]:your-org/changelog-marketplace.git
git push -u origin main

Detailed breakdown

  • What consumers run. Once the repo is public, anyone adds your marketplace and installs the plugin with two commands:

    /plugin marketplace add your-org/changelog-marketplace
    /plugin install changelog-helper@changelog-tools
    

    The GitHub owner/repo shorthand is the marketplace source. Because the plugin entry uses a relative source, Claude Code clones the repo and resolves ./plugins/changelog-helper inside its local copy.

  • Releasing an update. Change the skill, bump version to 1.1.0 in both plugin.json and marketplace.json (the validator warns if they drift), and push. Users pull the new version with /plugin marketplace update changelog-tools. If you had omitted version, every commit would register as a new version instead, which is convenient for a fast-moving internal tool but noisy for a public release.

  • Private distribution. To keep a plugin internal, host the marketplace repo privately. Teammates add it with the same command as long as their git credentials grant access; Claude Code uses the local git configuration to clone.

  • Community submission. To list in Anthropic’s public claude-community marketplace, run claude plugin validate . locally (the review pipeline runs the same check) and submit through the in-app form. Approved plugins are pinned to a commit and synced into the community catalog.


Recap

You built a distributable Claude Code skill from the inside out:

  1. A skill (SKILL.md) with a description that drives discovery, a bundled helper script, dynamic context injection, and scoped tool permissions.
  2. A plugin (plugin.json) that namespaces the skill and pins a version.
  3. A marketplace (marketplace.json) that catalogs the plugin behind an install command.
  4. Validation with claude plugin validate --strict, local testing with --plugin-dir, a full install through the local marketplace, and distribution by pushing to GitHub.

The layering is the point: the skill is the behavior, the plugin makes it shareable and versioned, and the marketplace makes it discoverable. A consumer goes from nothing to your skill with /plugin marketplace add and /plugin install.

Troubleshooting

  • “Plugin not found in any marketplace.” The marketplace is missing or stale. Run claude plugin marketplace update changelog-tools, or re-add it with claude plugin marketplace add ./. Confirm the marketplace name in the install command matches marketplace.json.
  • Skill does not appear after install. Run /reload-plugins, then /help. Check that skills/ sits at the plugin root, not inside .claude-plugin/ — the most common layout mistake.
  • Skill loads but the description is empty. The YAML frontmatter is malformed. Claude Code loads the body with empty metadata, so /name still works but auto-invocation fails. Launch with claude --debug to see the parse error.
  • The dynamic-context script does not run. Confirm the ```! block uses three backticks and that ${CLAUDE_SKILL_DIR} is spelled exactly. The block runs at invocation; check the script alone with make test-script.
  • Relative source fails for remote users. A relative source needs the whole repo cloned, so it works from a git or path marketplace but not from a marketplace added as a direct URL to marketplace.json. Use a github, url, or npm plugin source for URL-based distribution.

Next improvements

  • Add a second skill (for example, bump-version) to the same plugin by creating skills/bump-version/SKILL.md. No manifest change is required.
  • Wire validation into CI so claude plugin validate . --strict runs on every push and blocks a broken manifest from reaching users.
  • Pin the plugin source to a tag or SHA in marketplace.json for reproducible installs across a team.
  • Bundle a hook (hooks/hooks.json) so the plugin can, say, remind Claude to regenerate the changelog after a release commit.