Extract Plugin
End-to-end playbook for turning a cluster of personal ~/.claude/ items into a published plugin in the buvis-plugins marketplace. Walks through eleven phases with four user checkpoints. Codifies what we learned shipping audit-suite and strunk.
Dependencies
- Personal skills:
create-skill(Phase 8 runspython3 ~/.claude/skills/create-skill/scripts/validate_skill.py) - CLIs:
gh,git,python3,uv(uv run --with pytest), and bash >= 4 (scripts/check-readiness.shusesmapfile; macOS/bin/bash3.2 lacks it) - External repos:
~/git/src/github.com/buvis/claude-plugins(marketplace registration),claude-strunk(source of the copied LICENSE and.gitignore) - Reads
~/.claude/surfaces (hooks/,commands/,agents/,rules/,settings.json) and the shell rc files - Harness tool:
AskUserQuestion
Conventions
- Plugin repos live at
~/git/src/github.com/buvis/claude-<plugin-name>/. - Marketplace is the central
~/git/src/github.com/buvis/claude-plugins/repo. Each plugin gets one entry in its.claude-plugin/marketplace.json. - Initial version is always
0.1.0. Subsequent releases use the plugin's owndev/bin/release [patch|minor|major]. - Default branch is
master.
Phase 1: Intake
Confirm the cluster. The user will name a target (often from project_next_plugins_to_ship.md memory). Resolve it to a concrete list of skills, commands, hooks, and agents in ~/.claude/.
Output of this phase: the cluster list — concrete skill names (e.g. python-patterns python-testing rust-patterns). Substitute them for <skill1> <skill2> ... in every command below as you dispatch it.
If the user named hooks or commands directly, list those too. The cluster can include any of:
- Skills at
~/.claude/skills/<name>/ - Commands at
~/.claude/commands/<name>.md - Agents at
~/.claude/agents/<name>.md(directory absent until a custom agent is defined — skip when missing) - Hooks at
~/.claude/hooks/<name>.py(referenced from~/.claude/settings.json)
Phase 2: Readiness check (CHECKPOINT 1)
Run the completeness audit:
bash ~/.claude/skills/extract-plugin/scripts/check-readiness.sh <skill1> <skill2> ...
The script greps seven surfaces for inbound references to each cluster item, then runs the inverse check (what each cluster item references outward), plus two ambient probes:
| Surface | Why it matters |
|---|---|
| 1. Hooks (~/.claude/hooks/*.py + ~/.claude/settings.json) | A hook may invoke or depend on the cluster |
| 2. Commands (~/.claude/commands/*.md) | Slash commands often glue skills together |
| 3. Agents (~/.claude/agents/*.md) | Sub-agents may invoke cluster skills |
| 4. Other skills (~/.claude/skills/*/SKILL.md) | Cross-skill invocation via /name |
| 5. Rules (~/.claude/rules/**/*.md) | Workflow rules may name the cluster |
| 6. User instructions (~/.claude/{AGENTS,CLAUDE,GEMINI}.md) | These can carry multi-paragraph operational rules tied to the cluster (e.g. autopilot's "never skip review phases"). A plugin can't edit the user's CLAUDE.md — affected text must either ship as the plugin's own AGENTS.md or stay with the user as a documented residue |
| 7. Shell rc / shell plugins (~/.config/bash/**, ~/.zshrc, ~/.bashrc, ~/.config/fish/**) | Shell functions (e.g. autoclaude) can hard-code paths like ~/.claude/skills/<name>/... that break the moment the skill moves. Plugins can't install shell functions, so any hit here forces a distribution decision (ship as bin/ script, leave in dotfiles with updated paths, etc.) |
Inverse-check categories for what each cluster item points OUTWARD at:
- Other skill paths (
~/.claude/skills/<other>/...) - Slash commands (
/other-command) — find these by grepping for the leading slash followed by a known command name - Hardcoded state-file or sentinel paths shared with other subsystems (e.g.
dev/local/autopilot/state.jsonread by the autoclaude wrapper and tracon)
Ambient probes (run by the script, no per-item argument):
- Soft coupling probe: scan non-cluster hooks for string-literal references to cluster runtime artifacts — session-name keywords, env-var sentinels (
_AUTOPILOT_LOOP), state-file paths. Example:observe_tool.pymatches"autopilot"inCLAUDE_SESSION_NAME. These hooks don't move, but they create a hidden contract the plugin's session naming must preserve. - Personal-data probe: hard-coded
/Users/bob,buvis-plugins, etc. inside cluster items — must be sanitized before publish.
Classify each finding:
| Class | Action |
|---|---|
| Same cluster | Already covered — no action |
| Different cluster, already a plugin | Use namespaced reference (<otherplugin>:<skill>); document as runtime dep in README |
| Different cluster, not yet a plugin | Decide extraction order: ship the dependency plugin first, OR accept and document the dep, OR vendor the script (per Phase 5b) |
| Personal-only (observe_tool/instincts hooks, hardcoded buvis paths) | Exclude — won't ship |
| Built-in (/doctor, superpowers, etc.) | Ignore |
| Soft coupling in non-cluster hooks | Document as a stability contract in the plugin README (e.g. "session names must contain <keyword> for X to observe Y") |
| Shell-function hit | Make an explicit distribution decision before proceeding — see Phase 5d |
| User-instructions hit | Copy the text into the plugin's AGENTS.md, plan a follow-up cleanup of the user's CLAUDE.md/AGENTS.md |
| MISSING-FROM-PLUGIN | Expand cluster scope to include the missing hook/command/agent, then re-run check |
Stop here and confirm with the user: did the audit surface anything that needs to be added to the cluster, deferred, or distributed via a non-plugin channel? Do not proceed until the cluster is finalized and every shell-rc / user-instructions hit has an explicit plan.
Phase 3: Naming (CHECKPOINT 2)
Per the AGENTS.md naming convention (evocative single words over descriptive concatenations): propose 3-4 distinct evocative names with one-line rationale each. Avoid <adjective>-<noun>-suite patterns. Look for:
- Concrete objects (whetstone, anvil, lighthouse)
- Mythological figures (warden, sentinel, hermes, aegis)
- Action verbs (hone, plumb, forge)
- Insider jokes / homages (strunk for style guides)
- Single-word and pronounceable
Use AskUserQuestion with options. Capture the choice as the plugin name (lowercase, no spaces) and substitute it for <plugin-name> in every command below as you dispatch it. Commands in this playbook never rely on shell variables: each Bash call starts a fresh shell, so a variable set in one call is empty in the next.
Phase 4: Scaffolding
The repo path is ~/git/src/github.com/buvis/claude-<plugin-name> (e.g. /Users/bob/git/src/github.com/buvis/claude-warden). Substitute the concrete absolute path for <repo> in every command from here on as you dispatch it — never assign it to a shell variable. Shell state does not survive between Bash calls, and an unset variable silently turns a path like $REPO/skills into the root-anchored /skills.
Create the repo skeleton:
mkdir -p <repo>/.claude-plugin <repo>/skills <repo>/dev/bin
If the cluster includes commands, hooks, or agents, run one more mkdir -p naming exactly the extra dirs (<repo>/commands, <repo>/hooks, <repo>/agents).
Phase 5: Port components
For each item in the cluster:
Skills: cp -r ~/.claude/skills/<name> <repo>/skills/
Commands: cp ~/.claude/commands/<name>.md <repo>/commands/
Agents: cp ~/.claude/agents/<name>.md <repo>/agents/
Hooks: copy the script to <repo>/hooks/<name>.py, then create <repo>/hooks/hooks.json referencing it via ${CLAUDE_PLUGIN_ROOT}/hooks/<name>.py. Cross-check ~/.claude/settings.json to mirror the hook event/matcher.
Then apply three transforms:
5a. Path fixups
Skills with helper scripts must use ${CLAUDE_SKILL_DIR} instead of ~/.claude/skills/<name>/scripts/...:
rg -ln 'python3 ~/.claude/skills' <repo>/skills/
For each listed file, rewrite with the Edit tool: ~/.claude/skills/<this-skill>/scripts/foo.py → ${CLAUDE_SKILL_DIR}/scripts/foo.py (per-file, because the skill name in the path varies).
5b. Vendor cross-skill dependencies
If a skill in the cluster references scripts from a skill outside the cluster (e.g. audit-skills calling create-skill/scripts/validate_skill.py), copy the dependency into the dependent skill's own scripts/ directory. Note in CHANGELOG as a snapshot/vendor.
5c. Sanitize personal references
rg -ln '/Users/bob|buvis-plugins|~/bim/|/dev/local/' <repo>/skills/ <repo>/commands/ <repo>/agents/ <repo>/hooks/
Name only the directories that actually exist (rg errors on missing paths). Note rg alternation is |, not grep's \|.
Each hit is one of:
- Cosmetic example output in SKILL.md (e.g.,
/Users/bob/.claudein sample tables): replace with/Users/alice/.claudeor similar synthetic. - Real production reference in code: stop and ask the user.
Also clean caches repo-wide, not just under skills/*/scripts/:
- List them:
rg --files <repo> -g '*.pyc'(a hit inside any__pycache__/marks that directory for removal; shipped plugins have carried stray.pycunderlib/before). - Delete each with an explicit concrete path, e.g.
rm -rf <repo>/lib/__pycache__— before anyrm -rf, verify the path exists and sits inside<repo>; never run it with an empty or unsubstituted prefix (the fact-forcing gate may ask for confirmation). - Verify ignores:
rg -n '__pycache__|\.pyc' <repo>/.gitignore— add both patterns if missing.
5d. Shell-rc and user-instructions residue
If Phase 2 surfaced any shell-function or user-instructions hits, resolve them now — they can't ship as plugin files but they're part of the cluster's contract.
Shell functions (e.g. an autoclaude wrapping /run-autopilot in a loop):
- Plugins cannot install into
~/.zshrc/~/.bashrc/~/.config/bash/**. Pick one:- Ship as
bin/<name>script in the plugin repo. User sources or symlinks. Function body can reference${CLAUDE_PLUGIN_ROOT}/...if invoked with that env set, otherwise hard-code expected install paths. - Leave in dotfiles with paths updated to point at the plugin install location. Note that plugin install paths are version-suffixed (
~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/...) — use the install-path symlink or env discovery rather than literal version strings.
- Ship as
- Document the user-side install step in the plugin README ("Source
bin/<name>from your shell rc").
User instructions (paragraphs in ~/.claude/{AGENTS,CLAUDE,GEMINI}.md):
- Copy the relevant paragraphs into
<repo>/AGENTS.mdso plugin installers inherit them automatically. - Leave a short pointer in the user's own CLAUDE.md (e.g. "see
<plugin>plugin AGENTS.md") and queue the full-paragraph removal for Phase 11 cleanup.
Phase 6: Plugin metadata
Write these files. Use exact schemas:
.claude-plugin/plugin.json
{
"name": "<plugin-name>",
"version": "0.1.0",
"description": "<one-paragraph description, ~150 chars>",
"author": { "name": "buvis" }
}
.claude-plugin/marketplace.json (self-reference)
{
"name": "claude-<plugin-name>",
"description": "<short description>",
"owner": { "name": "buvis" },
"plugins": [
{
"name": "<plugin-name>",
"description": "<one-line>",
"version": "0.1.0",
"author": { "name": "buvis" },
"source": "./",
"category": "<category>"
}
]
}
LICENSE (MIT)
Copy from claude-strunk/LICENSE verbatim. Copyright year = current year. Holder = buvis.
.gitignore
Standard Python + editor + Claude Code local. Copy from claude-strunk/.gitignore.
README.md
Structure:
- Title + license shield
- Tagline / quote (1-2 lines, evocative)
- What's inside table — skill name → trigger keywords
- Install —
/plugin marketplace add buvis/claude-plugins+/plugin install <name>@buvis-plugins - Update —
/plugin update <name>@buvis-plugins - Alternative: install directly from this repo —
/plugin marketplace add buvis/claude-<name> - Why "<name>" (optional, if the name has a story — strunk does, audit-suite doesn't)
- License: MIT
CHANGELOG.md
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.1.0] - YYYY-MM-DD
### Added
- Initial release with N skills:
- `skill-name` — one-line summary
- ...
Phase 7: Release tooling
Copy the release script template:
cp ~/.claude/skills/extract-plugin/references/release-script.sh.template <repo>/dev/bin/release
Edit <repo>/dev/bin/release and set PLUGIN_NAME="<plugin-name>" near the top (this variable lives inside the release script file, where it is fine — the ban is on variables spanning the playbook's own Bash calls).
chmod +x <repo>/dev/bin/release
bash -n <repo>/dev/bin/release
Phase 8: Verification
For every skill in the new repo, validate its frontmatter AND its fenced
bash commands (the live-profile lints, PRD 00083) — one validator run per
skill directory, one command per Bash call (no shell loops; a for prefix
breaks permission matching):
python3 ~/.claude/skills/create-skill/scripts/validate_skill.py <repo>/skills/<skill1>
The bash lints are ERRORs: a skill that ships commands the live profile
denies (bare grep/find/cat/head/tail, standalone shell-variable
assignments, cd chains, bare stray script paths, author $VARs) fails
here. Fix the SKILL.md, never weaken the validator to admit it.
Vendored-validator re-sync rule. When the cluster vendors
validate_skill.py into a dependent skill's own scripts/ (Phase 5b), that
copy is a snapshot: re-vendor it from create-skill/scripts/validate_skill.py
at every extraction, and NEVER edit the vendored copy in place. The canonical
copy under create-skill is the only one that gets new lints; an edited
vendored copy silently drifts and stops catching what the canonical one does.
If any skill has helper scripts with tests, locate them:
rg --files <repo>/skills -g 'test_*.py'
Then run each test file's parent directory without cd:
uv run --directory <parent-dir> --with pytest python -m pytest -q
Phase 9: Git + GitHub remote (CHECKPOINT 3)
One command per Bash call, <repo> spelled concrete in each:
git -C <repo> init -b master
git -C <repo> add -A
git -C <repo> status --short # show user
git -C <repo> commit -m "feat: initial release with N skills [and commands/hooks]"
git -C <repo> tag -a v0.1.0 -m "v0.1.0 - initial release"
gh repo create buvis/claude-<plugin-name> --public --source <repo> --remote origin \
--description "<short description>"
Stop here and confirm with the user before pushing. Show the commit and the GitHub URL. After confirmation:
git -C <repo> push -u origin master
git -C <repo> push origin v0.1.0
Phase 10: Marketplace registration
Edit ~/git/src/github.com/buvis/claude-plugins/.claude-plugin/marketplace.json. Add a new entry to the plugins array:
{
"name": "<plugin-name>",
"source": {
"source": "url",
"url": "https://github.com/buvis/claude-<plugin-name>.git"
},
"description": "<description matching plugin.json>",
"version": "0.1.0"
}
Commit and push:
git -C ~/git/src/github.com/buvis/claude-plugins add .claude-plugin/marketplace.json
git -C ~/git/src/github.com/buvis/claude-plugins commit -m "feat: register <plugin-name> plugin v0.1.0"
git -C ~/git/src/github.com/buvis/claude-plugins push origin master
Phase 11: Local cleanup (CHECKPOINT 4)
Tell the user the migration commands:
/plugin marketplace update buvis-plugins
/plugin install <plugin-name>@buvis-plugins
After they confirm the published plugin works, suggest (don't auto-run) the cleanup:
rm -rf ~/.claude/skills/{<skill1>,<skill2>,...}
# plus any commands/hooks/agents that were ported
If the cluster included hooks: also instruct the user to remove the corresponding hooks entries from ~/.claude/settings.json (the plugin's hooks/hooks.json registers them now).
If Phase 2 found user-instructions residue: instruct the user to delete the migrated paragraphs from ~/.claude/{AGENTS,CLAUDE,GEMINI}.md and replace with a short pointer to the plugin.
If Phase 2 found a shell function: instruct the user to source the plugin's bin/<name> script from their shell rc (or update the in-place function's paths). Show the exact line to add.
Update memories
After successful publish, update project_next_plugins_to_ship.md to mark this cluster as shipped, and note any new candidates the readiness audit surfaced.
Failure modes to watch
${CLAUDE_SKILL_DIR}shows as empty: the bash invocation didn't originate from a skill context. Verify by running the failing command from inside Claude Code after a fresh install + restart.- Plugin install succeeds but skills don't appear: stale session. Run
/reload-pluginsor restart Claude Code. - Skill name collision: local copy still at
~/.claude/skills/<name>/after plugin install. Move locals to/tmp/backup/to test plugin isolation; remove when confirmed. - Central marketplace bump forgotten:
/plugin updatewon't see new versions. Always bump all three places (plugin.json, self-ref, central) — thedev/bin/releasescript handles this for subsequent versions.