| name | dist-extras |
| description | This skill should be used when the user asks to "publish to PyPI", "generate an install script", "curl install", "publish to Claude Code marketplace", "create marketplace.json", "PyPI trusted publishing", "private plugin", "plugin drift", "pin refresh", or needs help with non-Homebrew distribution channels (PyPI, curl-based installers, Claude Code marketplace). Covers Python package publishing, GitHub-release curl installers, Claude Code plugin marketplace listings (public and private), post-release pin-refresh, and drift detection. |
| category | distribution |
Distribution Extras (PyPI / curl / Marketplace)
Channel-specific expertise for distribution paths that aren't covered by the
Homebrew skills (homebrew-formula-expert, homebrew-multi-formula,
homebrew-setup-wizard, homebrew-workflow-expert). Three distinct channels
are grouped here because each one alone is too small to warrant its own
top-level skill, and they share a common pattern: declarative manifest +
GitHub Actions workflow + pre-flight validation.
Use distribution-strategist first to pick a channel. Use this skill once a
channel is chosen and you need execution detail.
Full walkthroughs (dry-run examples, workflow YAML templates, check tables) โ extracted
from the former standalone commands during the v4 consolidation (Phase 3.5, 2026-07-12):
references/pypi.md, references/curl-install.md, references/marketplace.md.
When to use which channel
| Channel | Best for | Avoid when |
|---|
| PyPI | Python libraries and CLIs; ecosystem-native pip install | Non-Python; binary-heavy distribution |
| curl install | Single-file CLIs with GitHub releases; quick-start docs | Security-sensitive envs (pipe-to-bash) |
| Marketplace | Claude Code plugins (.claude-plugin/plugin.json present) | Anything that isn't a Claude Code plugin |
PyPI publishing
Channel concern: build sdist+wheel, publish to PyPI, automate via GitHub
Actions, validate package metadata before release.
Subcommands (mirrors /craft:dist:pypi)
| Subcommand | Purpose |
|---|
validate | Check pyproject.toml, classifiers, README rendering, version sanity |
setup | Generate release workflow + walk user through trusted-publishing setup |
workflow | Generate .github/workflows/pypi-release.yml (release-triggered) |
publish | Build with uv build (or python -m build) and twine upload |
check | Pre-flight: clean tree, version bumped, tag matches, CHANGELOG entry |
Trusted publishing (preferred over API tokens)
PyPI's OIDC-based trusted publishing avoids long-lived tokens.
- On PyPI: project โ Publishing โ Add a new pending publisher
- Fields: GitHub owner/repo, workflow filename (
pypi-release.yml),
environment name (pypi)
- In workflow:
permissions: id-token: write + pypa/gh-action-pypi-publish@release/v1
- No secret needed โ OIDC handshakes the token at runtime
Build tools
- uv (preferred for new projects) โ
uv build produces both sdist + wheel
- build (canonical) โ
python -m build
- hatch / poetry / setuptools โ build backend declared in
pyproject.toml
Common failure modes
- Version on PyPI already exists (PyPI is append-only โ bump and retry)
- README has reST/Markdown content-type mismatch โ set
readme = "README.md"
- Missing classifiers (license, Python versions) flagged by
twine check
- Trusted publisher not registered before first run โ workflow fails at upload
curl install scripts
Channel concern: generate a portable install.sh that fetches a GitHub
release, detects platform, and installs to ~/.local/bin (or user-chosen
prefix). Optionally append install instructions to README.
Modes (mirrors /craft:dist:curl-install)
| Mode | Behavior |
|---|
--type binary | Download pre-built binary asset matching OS-ARCH |
--type source | Clone repo or fetch source tarball, build locally |
--type auto | Detect from repo (has release assets? โ binary; else source) |
preview | Print script to stdout without writing |
--update-readme | Append a curl one-liner to README under "Installation" |
Script invariants
Every generated script must:
- Start with
set -euo pipefail
- Detect
uname -s and uname -m, normalize x86_64โamd64, aarch64|arm64โarm64
- Fetch latest tag from
api.github.com/repos/<owner>/<repo>/releases/latest
- Verify checksum (SHA256) against published checksum file when available
- Default
INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/bin}", allow override
- Use color helpers (
info/success/error) that degrade on non-tty
Security notes
Curl-pipe-bash is a known anti-pattern in hardened environments. For users
who object, the generated script should also work via the canonical pattern:
curl -fsSL <url>/install.sh -o install.sh
less install.sh
bash install.sh
Document both invocation styles in the README block.
Claude Code marketplace
Channel concern: list a Claude Code plugin in a GitHub-hosted marketplace
that users add with claude plugin marketplace add <owner>/<repo>.
Subcommands (mirrors /craft:dist:marketplace)
| Subcommand | Purpose |
|---|
init | Generate .claude-plugin/marketplace.json from plugin.json |
validate | Schema-check both plugin.json and marketplace.json |
test | Local install/uninstall cycle via claude plugin marketplace add |
publish | Push to GitHub default branch (marketplace polls the repo) |
marketplace.json minimal shape
{
"name": "{org}-{plugin}",
"owner": { "name": "...", "email": "..." },
"metadata": { "description": "...", "version": "..." },
"plugins": [{
"name": "{plugin-name}",
"source": { "source": "github", "repo": "{owner}/{repo}" },
"description": "...",
"version": "...",
"homepage": "...",
"repository": "...",
"license": "MIT",
"category": "development",
"keywords": []
}]
}
Critical limitation: no build step on install
claude plugin marketplace add clones the repo โ it does not run
npm install, npm run build, or any build step. Implications:
- Skills (
.md) and commands (.md) work โ they're inert files
- Agents (
.md) work โ same
- MCP servers with a gitignored
dist/ won't work โ the server binary
isn't on disk after clone. Solution: publish the MCP server to npm and
reference via npx -y @scope/package in .mcp.json.
- Anything requiring a compile step must ship compiled or use a package
registry as the actual delivery channel.
Validation checklist
Public vs private plugin mode
By default dist:marketplace targets a public shared marketplace. Private plugins
(closed-source, PII-carrying) must never appear there.
Auto-detect: gh repo view --json visibility -q .visibility returns PUBLIC or PRIVATE.
When the repo is PRIVATE, behave as if --private were passed โ no flag required.
--private behaviour (or auto-detected private repo):
Scaffold a self-marketplace inside the plugin's own repo rather than the shared public one:
.claude-plugin/marketplace.json โ single-plugin manifest; source: "./"
Minimal self-marketplace shape:
{
"name": "{plugin-name}-marketplace",
"owner": { "name": "...", "email": "..." },
"plugins": [{
"name": "{plugin-name}",
"source": { "source": "./", "path": "." },
"description": "...",
"version": "..."
}]
}
Emit the private install recipe to stdout after scaffolding:
claude plugin marketplace add <owner>/<repo>
claude plugin install <plugin-name>@<owner>/<repo>
Hard guard: If repo is PRIVATE and publish targets a public marketplace, abort:
ERROR: Repository is PRIVATE. Cannot publish to public marketplace.
Use --private (or let auto-detect run) to scaffold a self-marketplace instead.
Public repo (default): behaviour unchanged โ appends plugin entry to the shared
marketplace.json and pushes.
Known upstream limitation: claude plugin install uses SSH, not HTTPS
claude plugin install <name>@<marketplace> clones via git@github.com:...
unconditionally โ even for fully public repos, and even on machines with a
working gh-managed HTTPS credential helper. Unlike claude plugin marketplace add (which correctly falls back to HTTPS when SSH auth fails),
install has no HTTPS fallback, so it fails outright with Permission denied (publickey) on any machine without SSH keys registered for GitHub.
This is a Claude Code CLI bug, not a marketplace/manifest problem โ
nothing in marketplace.json's schema can work around it (confirmed live
2026-07-02: craft's data-wise marketplace entry was correctly registered
and installable-in-principle, but install still failed via SSH). Tracked
upstream: anthropics/claude-code#26588,
#52234,
#29722,
#18001 (multiple
duplicate reports, open as of April 2026).
Workaround (per-machine, not per-repo โ apply once):
git config --global url."https://github.com/".insteadOf git@github.com:
Redirects any git@github.com: clone to https://github.com/, so it
transparently uses the existing gh auth git-credential helper (already
installed by gh auth login) instead of requiring an SSH key. Verified
live: craft@data-wise installed successfully after applying this rule,
having failed before it.
Post-release pin refresh (drift prevention)
After tagging/releasing, installed Claude Code pins do not auto-update. Without an explicit
refresh, every local installation silently stays on the old version.
Always emit these two commands at the end of a release:
claude plugin marketplace update <marketplace-name>
claude plugin update <plugin-name>@<marketplace>
For private self-marketplaces, <marketplace-name> is <owner>/<repo>.
Drift doctor (dist:marketplace validate --drift):
Compares the canonical source version in plugin.json against the installed pin in
~/.claude/installed_plugins.json and warns on any mismatch.
plugin_version=$(jq -r .version .claude-plugin/plugin.json)
installed_pin=$(jq -r --arg name "<plugin-name>" \
'.plugins | to_entries[] | select(.value.name == $name) | .value.version' \
~/.claude/installed_plugins.json 2>/dev/null)
if [ "$plugin_version" != "$installed_pin" ]; then
echo "WARN: source $plugin_version vs pin $installed_pin"
echo " Fix: claude plugin marketplace update <mkt>"
echo " claude plugin update <name>@<mkt>"
fi
Embed this drift check in:
dist:marketplace validate output (add --drift flag)
- Release pipeline Step 13.6 advisory WARN block
- Any
fix-local-plugins.sh-style maintenance scripts (generalize rather than per-plugin)
Cowork & Desktop surfaces
The craft plugin ships to three independent surfaces. This section covers the Cowork and Desktop
surfaces โ the Code surface (Claude Code CLI) is handled by the Homebrew and marketplace channels
above.
The 3-surface model
| Surface | Runtime | Distribution channel | Gate |
|---|
| Code | Claude Code CLI | Homebrew tap + GitHub marketplace | BLOCK |
| Cowork | Cowork platform | Cowork plugin registry (skills-first) | WARN |
| Desktop | Claude Desktop app | Desktop plugin store (DXT) | INFO |
A release is "fully shipped" when all BLOCK-gated surfaces report the expected version.
WARN surfaces (Cowork, brew-installed, Code-registered) are surfaced in the report but do not
gate the automated pipeline.
Registry vs. user model: The registry.json tracks 8 surfaces; the user-facing "3-surface"
view collapses them. Code aggregates git-tag, marketplace, tap, brew, code-registered, and
aggregator; Cowork maps to the cowork surface (WARN); Desktop maps to desktop-ext (INFO).
Code surface
Distributed via two channels (both BLOCK-gated):
- Homebrew tap (
data-wise/tap) โ brew install data-wise/tap/craft
- GitHub marketplace โ
claude plugin marketplace add Data-Wise/craft
The aggregator-sync.yml CI action keeps the Data-Wise aggregator marketplace in sync on every
release: published event (auto-merge PR, fail-loud on failure).
Cowork surface
Cowork ships a skills-first subset of craft. The surface tracks craft via a separate GUI plugin
registry. Key behaviors:
- Skills-first: Cowork consumes
skills/ and commands/ but may present them differently
than Claude Code CLI.
- Personal vs Org marketplace: Cowork supports both personal and org-level marketplaces.
The craft plugin is published under the Data-Wise org marketplace.
- Install path:
claude plugin marketplace add data-wise/craft (Cowork variant) โ the exact
path is marketplace-specific and may differ from the Code CLI install command.
- Manual update: There is no automated propagation from the release pipeline to the Cowork
store. After a release, the Cowork surface requires a manual store update. The pipeline emits
a WARN with the Cowork report and a remind.
Private plugin install on Cowork
For plugins not in the public marketplace (git-gated, private repo):
- Cowork clones the repo directly โ the runtime needs read access (GitHub PAT or SSH key
configured in the Cowork environment).
- Install command:
claude plugin marketplace add <owner>/<private-repo>, then
claude plugin install <name>@<owner>/<private-repo>.
- Post-release refresh is manual: include the add+install recipe in the pipeline WARN block
for the Cowork surface entry.
Desktop surface
The Claude Desktop app ships plugins via its own DXT plugin store. Key behaviors:
- DXT format: Desktop plugins use the
.dxt format (different from the Code CLI plugin format).
- INFO only: The Desktop surface is informational in the registry โ no automated verify or
block gate. The pipeline reports it but does not attempt automated verification.
- Manual gate: Before shipping, verify manually that the Desktop store version matches the
release version.
- Private plugin on Desktop: No native private-repo install path exists. Workaround: clone
the repo locally and sideload from the filesystem if the Desktop app supports local plugin
paths; otherwise, request a DXT store listing (gated by Anthropic). Emit an INFO note in the
release pipeline for plugins with a Desktop surface entry.
Surface registry
The scripts/surfaces/registry.json file is the source of truth for all surfaces. Use
/craft:dist:surfaces to view the full surface matrix and current gate states.
/craft:dist:surfaces
/craft:dist:surfaces --json
Integration
Use with:
distribution-strategist skill โ pick the right channel first
homebrew-* skills โ when the project also distributes via Homebrew
/craft:check --for release โ pre-release validation across channels
/craft:code:release โ orchestrates version bump โ tag โ release across channels
Cross-channel patterns
- One canonical version: derive from a single source (
pyproject.toml,
plugin.json, package.json); never hand-edit in multiple files
- Tag triggers the workflow: prefer GitHub Release published events
over push-to-main for release workflows
- Pre-flight before publish: each channel has a
validate/check
subcommand โ run it in CI before the release job