Complete workflow for publishing a new version to GitHub, PyPI, and the Claude Code marketplace.
Use semantic versioning — MAJOR.MINOR.PATCH:
| Change type | Bump | Example |
|---|---|---|
| Bug fix, typo, small tweak | PATCH | 1.1.1 → 1.1.2 |
| New feature, new skill, new CLI command | MINOR | 1.1.2 → 1.2.0 |
| Breaking change (renamed tools, changed API, removed features) | MAJOR | 1.2.0 → 2.0.0 |
Rules of thumb:
- If a user's existing workflow still works after updating → PATCH or MINOR
- If a user has to change anything on their end → MAJOR
- Skill content improvements (better prompts, better thresholds) → PATCH
- New MCP tools or CLI commands → MINOR
- Renamed or removed MCP tools → MAJOR
- All changes committed and pushed to main
- Tests pass:
uv run pytest tests/ -x -q - Linting clean:
uv run ruff check src/ tests/ packages/ - Format clean:
uv run ruff format --check src/ tests/ packages/ - Version number updated in both places (see below)
Use uv run ruff (not uv tool run ruff) so checks run the ruff version
pinned in the dev dependencies — the same one CI and the pre-push hook use.
Update the version in three files:
# 1. pyproject.toml (line 6)
version = "X.Y.Z"
# 2. plugin/.claude-plugin/plugin.json
"version": "X.Y.Z"
# 3. .claude-plugin/marketplace.json -- plugins[0].source.ref, with a "v" prefix
"ref": "vX.Y.Z"The third is easy to miss and the test suite enforces it:
tests/test_cli_setup.py::TestMarketplaceVersionPin fails when the marketplace
ref does not equal v + the plugin version, so the pre-release checks above
will not pass until all three agree.
uv.lock also records the project version. Any uv run after the bump rewrites
it, so it shows up as an unstaged change — commit it with the rest rather than
leaving the lock disagreeing with pyproject.toml.
Commit:
git add pyproject.toml plugin/.claude-plugin/plugin.json \
.claude-plugin/marketplace.json uv.lock
git commit -m "chore: bump version to X.Y.Z"git tag vX.Y.Z
git push && git push origin vX.Y.Zuv build
uv publish --token "$(security find-generic-password -a pypi -s pypi -w)"Without file arguments, uv publish uploads every file in dist/ —
including artifacts left over from earlier releases. Clear dist/ before
building, or pass explicit paths: uv publish --token ... dist/phantom_audio-X.Y.Z*.
Sibling package: the stem-separation plugin lives in
packages/phantom-audio-separation (its own version in
packages/phantom-audio-separation/pyproject.toml). When it has changes to
release, build and publish it too:
uv build --package phantom-audio-separation
uv publish --token "$(security find-generic-password -a pypi -s pypi -w)" dist/phantom_audio_separation-*Verify it works:
uv tool install phantom-audio --python 3.13 --force
phantom --versionThere is no "notify Anthropic" step. That was wrong. Anthropic's community
catalog pins each plugin to a commit SHA on your default branch and a nightly
CI sweep (07:23 UTC) opens a bump/<name> PR when your main moves ahead. A
human on their side merges it, and the public directory syncs nightly after that.
Normal turnaround is one to three days.
Two consequences worth internalizing:
- They track
main, not our tag. Their entry carries"ref": "main"plus a pinnedsha. Our own.claude-plugin/marketplace.jsonsays"ref": "v1.5.0", and the D-01 test asserts that matches the tag andplugin.json. That invariant governs our marketplace only — the Anthropic catalog never reads it. - The version users see comes from
plugin/.claude-plugin/plugin.jsonat the pinned SHA. So bumping that file and pushing tomainis normally all it takes.
Check the pin after every release:
curl -sSL https://raw.githubusercontent.com/anthropics/claude-plugins-community/main/.claude-plugin/marketplace.json \
| python3 -c "import json,sys; p=[x for x in json.load(sys.stdin)['plugins'] if x['name']=='phantom'][0]; print(p['source'])"
git rev-list --count <that-sha>..main # 0 means the pin is currentIf the pin is stale, first run the same validation their sweep runs — a failure there produces no PR and no notification, the pin just silently holds:
claude plugin validate ./plugin --strictIf validation passes and the pin still has not moved after a few days, re-submit
through https://clau.de/plugin-directory-submission and say in the notes which
SHA it is stuck on. Do not open a PR against anthropics/claude-plugins-community
— external PRs are auto-closed.
Directory description, homepage and category come from the submission record, not
the repo. Editing the repo will not change them. Never change the plugin's name;
it breaks existing installs.
Separately, update .claude-plugin/marketplace.json so users who add
fadelabs/phantom directly get the new version:
# ref = the tag name
git rev-parse HEAD # if you also want to record a shaUpdate the ref, commit, and push.
| What | Where | Command |
|---|---|---|
| Version (package) | pyproject.toml line 6 |
edit manually |
| Version (plugin) | plugin/.claude-plugin/plugin.json |
edit manually |
| Version (marketplace ref) | .claude-plugin/marketplace.json |
edit manually, vX.Y.Z |
| Tests | local | uv run pytest tests/ -x -q |
| Lint | local | uv run ruff check src/ tests/ packages/ |
| Build | local | uv build |
| Publish to PyPI | pypi.org | uv publish --token "$(security find-generic-password -a pypi -s pypi -w)" |
| Tag | GitHub | git tag vX.Y.Z && git push origin vX.Y.Z |
| Install test | local | uv tool install phantom-audio --python 3.13 --force |
# 1. Make sure everything is clean
uv run pytest tests/ -x -q
uv run ruff check src/ tests/ packages/
# 2. Bump version (edit both files)
# pyproject.toml: version = "1.2.0"
# plugin/.claude-plugin/plugin.json: "version": "1.2.0"
# 3. Commit, tag, push
git add pyproject.toml plugin/.claude-plugin/plugin.json
git commit -m "chore: bump version to 1.2.0"
git tag v1.2.0
git push && git push origin v1.2.0
# 4. Build and publish to PyPI
uv build
uv publish --token "$(security find-generic-password -a pypi -s pypi -w)"
# 5. Verify
uv tool install phantom-audio --python 3.13 --force
phantom --version # should show 1.2.0
# 6. Update marketplace sha (if applicable)
# Edit .claude-plugin/marketplace.json with new ref/sha
# git add, commit, push