Resync Local Plugins
This repo's cyberplace Claude Code marketplace is a directory source pointing at the local working tree. A directory marketplace snapshots at the committed git HEAD, not the working tree — so uncommitted plugin/skill edits never go live, and installed plugins stay pinned to whatever HEAD was current when they were last installed. Run this after committing plugin/skill changes to move the pin forward.
Not every plugin is a directory source. Each entry in .claude-plugin/marketplace.json declares its own source, and an npm-source plugin installs from the registry — this skill cannot move it to your local HEAD at all. Read the manifest before assuming a resync will help:
ROOT="$(git rev-parse --show-toplevel)"
node -e 'const m = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"))
for (const p of m.plugins)
console.log(p.name.padEnd(14), typeof p.source === "string" ? p.source : JSON.stringify(p.source))' \
"$ROOT/.claude-plugin/marketplace.json"
Anything not of the form ./plugins/<name> needs a release, not a resync. See the npm-source note below.
Preconditions
Steps
Re-snapshot the marketplace to current HEAD:
claude plugin marketplace update cyberplace
Re-pin every installed @cyberplace plugin. Plain install is idempotent and will NOT move the pin — you must uninstall then install:
for p in $(claude plugin list 2>/dev/null | grep -oE '[a-z-]+@cyberplace' | cut -d@ -f1 | sort -u); do
claude plugin uninstall "${p}@cyberplace"
claude plugin install "${p}@cyberplace"
done
Verify by content, not by metadata. Each @cyberplace key in installed_plugins.json maps to an array of install records across scopes — index 0 is usually a stale scope: "project" pin from an old cyberplace.worktrees/* checkout, so reading it reports failures that aren't real. Filter to scope: "user", then diff the installed tree against the source:
ROOT="$(git rev-parse --show-toplevel)"
HEAD=$(git -C "$ROOT" rev-parse --short=12 HEAD)
node -e '
const fs = require("fs")
const root = process.argv[1]
const inst = JSON.parse(fs.readFileSync(process.env.HOME + "/.claude/plugins/installed_plugins.json", "utf8"))
const mkt = JSON.parse(fs.readFileSync(root + "/.claude-plugin/marketplace.json", "utf8"))
const kind = Object.fromEntries(mkt.plugins.map(p => [p.name, typeof p.source === "string" ? "dir" : "npm"]))
for (const [key, recs] of Object.entries(inst.plugins)) {
if (!key.endsWith("@cyberplace")) continue
const name = key.slice(0, -"@cyberplace".length)
const rec = (recs || []).find(r => r.scope === "user")
if (!rec) { console.log([name, "-", "-", "NO-USER-INSTALL"].join("\t")); continue }
console.log([name, kind[name] || "?", (rec.gitCommitSha || "-").slice(0, 12), rec.installPath].join("\t"))
}' "$ROOT" | while IFS=$'\t' read -r name knd sha dir; do
case "$knd" in
npm) pin="npm source — cannot track HEAD" ;;
dir) [ "$sha" = "$HEAD" ] && pin="pin OK" || pin="PIN STALE ($sha)" ;;
*) pin="$sha" ;;
esac
# diff -rq emits "Files A and B differ", so exclusions must match mid-line, not at $
n=$(diff -rq "$dir" "$ROOT/plugins/$name" 2>/dev/null \
| grep 'differ' \
| grep -vE '\.turbo/|node_modules/|/package\.json and ' | wc -l)
printf '%-14s %-32s content-diffs=%s\n' "$name" "$pin" "$n"
done
echo "HEAD: $HEAD"
content-diffs=0 is the pass condition for every plugin, directory- and npm-sourced alike. Directory-sourced plugins must also read pin OK. The exclusions are deliberate: .turbo/ and node_modules/ are local build state, and npm rewrites package.json on publish — none of the three indicate drift.
Tell the user to run /reload-plugins. This reloads every active plugin's code from disk into the running process — plugins, skills, agents, hooks, and plugin MCP/LSP servers — so the re-pin takes effect without a restart. /new and /clear do NOT reload plugins; /reload-skills only refreshes skill text, not plugin code/agents/hooks. Caveats: if a re-pinned plugin ships an MCP server the reload may invalidate the prompt cache on the next request — pass /reload-plugins --force to push it through. A full quit-and-relaunch of the claude process is the fallback if /reload-plugins misbehaves.
Notes
- Do not push or commit anything here — this only touches local plugin install state (
~/.claude/plugins/), never the repo.
- If a plugin fails to reinstall, report which one and stop; do not leave the set half-pinned.
npm-source plugins cannot be resynced
A plugin declared as {"source":"npm","package":"<pkg>"} installs from the registry, so no amount of marketplace update + reinstall will pick up local edits. Shipping a change to one means releasing it: changeset → version PR → release workflow → claude plugin uninstall/install <name>@cyberplace.
Two tells that you are looking at an npm-source plugin, both visible in installed_plugins.json:
- its user-scope record has no
gitCommitSha while every directory-sourced sibling has one
- its cache directory is named for the npm version (
…/sdd/0.0.0) rather than a commit sha
Do not trust the recorded version
Claude Code keys the cache directory on the version string, and reuses a directory whose name it already has — it has been observed writing 0.1.0 content into a directory still named 0.0.0, leaving version: "0.0.0" in the install record. The directory name and the version field are therefore both unreliable. The content diff in step 3 is the only honest check.
1---2name: resync-local-plugins3description: Use this skill after committing changes to this repo's plugins or skills, to re-pin the local-directory cyberplace marketplace so the installed plugins reflect the new HEAD. Triggers: 'resync the plugins', 'the marketplace is stale', 'my skill edit isn't loading', 'update the local plugins', after landing a commit under plugins/.4---56# Resync Local Plugins78This repo's `cyberplace` Claude Code marketplace is a **`directory` source** pointing at the local working tree. A directory marketplace snapshots at the **committed git HEAD**, not the working tree — so uncommitted plugin/skill edits never go live, and installed plugins stay pinned to whatever HEAD was current when they were last installed. Run this after committing plugin/skill changes to move the pin forward.910**Not every plugin is a directory source.** Each entry in `.claude-plugin/marketplace.json` declares its own `source`, and an **npm-source** plugin installs from the registry — this skill cannot move it to your local HEAD at all. Read the manifest before assuming a resync will help:1112```bash13ROOT="$(git rev-parse --show-toplevel)"14node -e 'const m = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"))15for (const p of m.plugins)16 console.log(p.name.padEnd(14), typeof p.source === "string" ? p.source : JSON.stringify(p.source))' \17 "$ROOT/.claude-plugin/marketplace.json"18```1920Anything not of the form `./plugins/<name>` needs a **release**, not a resync. See the npm-source note below.2122## Preconditions2324- Your plugin/skill edits are **committed** (this reads HEAD, not the working tree). Uncommitted edits are invisible — commit first.25- The `cyberplace` marketplace is a directory source at this repo. Confirm:26 ```bash27 claude plugin marketplace list # cyberplace → Source: Directory (…/cyberplace)28 ```29 If it still points at GitHub, repoint it first: `claude plugin marketplace remove cyberplace && claude plugin marketplace add "$(git rev-parse --show-toplevel)"`.3031## Steps32331. **Re-snapshot the marketplace** to current HEAD:34 ```bash35 claude plugin marketplace update cyberplace36 ```37382. **Re-pin every installed `@cyberplace` plugin.** Plain `install` is idempotent and will NOT move the pin — you must uninstall then install:39 ```bash40 for p in $(claude plugin list 2>/dev/null | grep -oE '[a-z-]+@cyberplace' | cut -d@ -f1 | sort -u); do41 claude plugin uninstall "${p}@cyberplace"42 claude plugin install "${p}@cyberplace"43 done44 ```45463. **Verify by content, not by metadata.** Each `@cyberplace` key in `installed_plugins.json` maps to an **array of install records across scopes** — index 0 is usually a stale `scope: "project"` pin from an old `cyberplace.worktrees/*` checkout, so reading it reports failures that aren't real. Filter to `scope: "user"`, then diff the installed tree against the source:4748 ```bash49 ROOT="$(git rev-parse --show-toplevel)"50 HEAD=$(git -C "$ROOT" rev-parse --short=12 HEAD)51 node -e '52 const fs = require("fs")53 const root = process.argv[1]54 const inst = JSON.parse(fs.readFileSync(process.env.HOME + "/.claude/plugins/installed_plugins.json", "utf8"))55 const mkt = JSON.parse(fs.readFileSync(root + "/.claude-plugin/marketplace.json", "utf8"))56 const kind = Object.fromEntries(mkt.plugins.map(p => [p.name, typeof p.source === "string" ? "dir" : "npm"]))57 for (const [key, recs] of Object.entries(inst.plugins)) {58 if (!key.endsWith("@cyberplace")) continue59 const name = key.slice(0, -"@cyberplace".length)60 const rec = (recs || []).find(r => r.scope === "user")61 if (!rec) { console.log([name, "-", "-", "NO-USER-INSTALL"].join("\t")); continue }62 console.log([name, kind[name] || "?", (rec.gitCommitSha || "-").slice(0, 12), rec.installPath].join("\t"))63 }' "$ROOT" | while IFS=$'\t' read -r name knd sha dir; do64 case "$knd" in65 npm) pin="npm source — cannot track HEAD" ;;66 dir) [ "$sha" = "$HEAD" ] && pin="pin OK" || pin="PIN STALE ($sha)" ;;67 *) pin="$sha" ;;68 esac69 # diff -rq emits "Files A and B differ", so exclusions must match mid-line, not at $70 n=$(diff -rq "$dir" "$ROOT/plugins/$name" 2>/dev/null \71 | grep 'differ' \72 | grep -vE '\.turbo/|node_modules/|/package\.json and ' | wc -l)73 printf '%-14s %-32s content-diffs=%s\n' "$name" "$pin" "$n"74 done75 echo "HEAD: $HEAD"76 ```7778 **`content-diffs=0` is the pass condition** for every plugin, directory- and npm-sourced alike. Directory-sourced plugins must *also* read `pin OK`. The exclusions are deliberate: `.turbo/` and `node_modules/` are local build state, and npm rewrites `package.json` on publish — none of the three indicate drift.79804. **Tell the user to run `/reload-plugins`.** This reloads every active plugin's code from disk into the running process — plugins, skills, agents, hooks, and plugin MCP/LSP servers — so the re-pin takes effect without a restart. `/new` and `/clear` do NOT reload plugins; `/reload-skills` only refreshes skill text, not plugin code/agents/hooks. Caveats: if a re-pinned plugin ships an MCP server the reload may invalidate the prompt cache on the next request — pass `/reload-plugins --force` to push it through. A full quit-and-relaunch of the `claude` process is the fallback if `/reload-plugins` misbehaves.8182## Notes8384- Do not push or commit anything here — this only touches local plugin install state (`~/.claude/plugins/`), never the repo.85- If a plugin fails to reinstall, report which one and stop; do not leave the set half-pinned.8687### npm-source plugins cannot be resynced8889A plugin declared as `{"source":"npm","package":"<pkg>"}` installs from the registry, so **no amount of `marketplace update` + reinstall will pick up local edits**. Shipping a change to one means releasing it: changeset → version PR → release workflow → `claude plugin uninstall/install <name>@cyberplace`.9091Two tells that you are looking at an npm-source plugin, both visible in `installed_plugins.json`:9293- its user-scope record has **no `gitCommitSha`** while every directory-sourced sibling has one94- its cache directory is named for the npm version (`…/sdd/0.0.0`) rather than a commit sha9596### Do not trust the recorded version9798Claude Code keys the cache directory on the version string, and **reuses a directory whose name it already has** — it has been observed writing `0.1.0` content into a directory still named `0.0.0`, leaving `version: "0.0.0"` in the install record. The directory name and the `version` field are therefore both unreliable. The content diff in step 3 is the only honest check.