description: Prefer globally installed skills over local ones. Reconcile the repository-local skill lock against the global one and remove local duplicates. agent: general subtask: true meta: gist-source: "https://gist.github.com/diegohb/e6a26a4027fc624a45f0f6f862bc1457"
Keep one shared version of each skill by preferring the global install. If a skill exists in both local and global locks, remove the local duplicate.
skills CLI versions vary in lock-file names and per-entry fields. These instructions handle that by probing both lock names and matching only by skill-name keys.
{
"version": <number>, // observed: 1 or 3 — preserve whatever is present
"skills": {
"<skill-name>": { // per-entry fields vary (computedHash | skillFolderHash,
"source": "...", // sourceUrl, installedAt, updatedAt, pluginName...).
"skillPath": "...", // DO NOT depend on these — match by name key only.
...
}
}
}- Local lock:
skills-lock.jsonin the repository root → fallback.agents/skills-lock.json→ fallback.agents/.skill-lock.json. - Global lock:
~/.agents/skills-lock.json→ fallback~/.agents/.skill-lock.json.
- Local:
.agents/skills/<skill-name>/ - Global:
~/.agents/skills/<skill-name>/(read-only reference; never modify).
Perform these steps in order. Do not improvise file paths or names.
-
Resolve paths. Expand
~to the user's home directory for global paths. Do not assume a specific OS shell. -
Locate the local lock file by probing the local locations above, in order.
- If none exists → stop and report: "No local skills lock file found (checked
skills-lock.json,.agents/skills-lock.json,.agents/.skill-lock.json); nothing to reconcile."
- If none exists → stop and report: "No local skills lock file found (checked
-
Locate the global lock file by probing the global locations above, in order.
- If none exists → stop and report: "No global skills lock file found (checked
~/.agents/skills-lock.json,~/.agents/.skill-lock.json); cannot reconcile. Install skills globally first withnpx skills add --scope global."
- If none exists → stop and report: "No global skills lock file found (checked
-
Parse both files as JSON. If either parse fails, stop and report the error verbatim with the file path. Do not repair corrupted JSON.
-
Build two sets of skill names from the keys of the
skillsobject:localNames= keys oflocal.skillsglobalNames= keys ofglobal.skills- If a file has no
skillsobject, treat it as an empty set and report that.
-
Compute
toRemove=localNames ∩ globalNames(skills present in both).- If
toRemoveis empty → stop and report: "No local skills duplicated in the global lock; nothing to remove."
- If
-
Capture SKILL.md mtimes — READ-ONLY, before deletion (step 9). Step 9 removes local
SKILL.mdfiles, so capture mtimes now. For each skill intoRemove, generate two listings keyed by skill name (local and global) using the procedure below. Store aslocalMtimesandglobalMtimes. This step does no writes.⚠️ mtimes are unreliable for freshness.SKILL.mdmtime reflects when the file was extracted to disk, which tracks installation order, not source repo freshness. Two installs at the same git commit get different mtimes (the earlier install wins the earlier mtime). Always use the GitHub commit comparison from step 8 for the regression gate; mtimes are captured only for the report table. -
Fetch GitHub commit hashes for each skill — authoritative freshness signal. For each skill in
toRemove, query the GitHub API for the latest commit that touched the skill'sskillPathin the source repo. Store the result asgithubCommits[skillName] = { sha, date }. This step is read-only and network-bound.API call per skill:
GET https://api.github.com/repos/{owner}/{repo}/commits?path={skillPath}&per_page=1Extract from the response:
sha: full 40-char commit SHA (shorten to 12 chars for display)date:commit.committer.date— the authoritative last-modified instant of this skill's content in the source repo
If the API call fails (rate limit, network error, repo not found), record
{ sha: "error", date: null }for that skill and continue. A failed fetch for some skills does not stop the procedure; those skills appear as ⚪unknownin the report.Use the GitHub commit SHA from the global lock's
skillFolderHash(if available) or the local lock'scomputedHashas a fast-path equality check before making the API call:- If
local.computedHash === global.skillFolderHash(or equivalent content-hash equality), both installs are at identical content — record SHA as"same"and skip the API call for this skill. - If the hashes differ, make the API call to determine which commit is newer.
Build two lookups from this data:
localGithubCommit[skillName]— the commit sha/date this skill's local content corresponds to (uselocal.computedHash+ GitHub API if needed).globalGithubCommit[skillName]— the commit sha/date this skill's global content corresponds to (useglobal.skillFolderHash+ GitHub API if needed).
If a lock entry lacks
skillFolderHash/computedHash(e.g., schema version 1 without a hash field), fall back to the API call using the skill'sskillPath. -
Run a regression preflight check (report + gate), using GitHub commit dates.
Why commit dates, not mtimes? Two installs at the same git commit produce different SKILL.md mtimes because the earlier installation wins the earlier mtime. Mtimes track installation order, not source freshness. Commit dates from GitHub API track actual content changes in the source repo — the only signal that determines whether global is older than local.
comparablePairs: skills intoRemovewhere bothlocalGithubCommit[skill].shaandglobalGithubCommit[skill].shaare known and not"error". This includes"same"SHA pairs — identical content is a valid, meaningful comparison.regressionPairs:comparablePairswhereglobalGithubCommit[skill].shais strictly older thanlocalGithubCommit[skill].sha(i.e., the SHA strings differ and the global commit date is earlier)."same"SHA pairs are never regressions because identical content cannot be older or newer than itself.- If
comparablePairsis non-empty andregressionPairs.count > comparablePairs.count / 2, stop before deletion and report: "Stopped: most comparable duplicated skills have a newer local commit than the global one; reconcile global installs first (npx skills add --scope global )."
Note: A skill where both installs are at the same GitHub SHA (
localSha === globalSha) is never a regression, because identical content cannot be older or newer than itself — regardless of installation timestamps. The step-9 gate cannot fire on same-SHA pairs. -
For each skill in
toRemove, remove it from the local install with per-skill atomicity:- Delete the local skill directory
.agents/skills/<skill-name>/if it exists. - Delete the key from
local.skills. - Verify both: the key is gone from the in-memory
local.skills, and the directory no longer exists (or was already missing). If verification fails for a skill, stop immediately and report exactly which skill failed and which check failed.
- Delete the local skill directory
-
Write the updated local lock file to the same path read in step 2. Serialize with 2-space indentation and a trailing newline, preserving the original
versionfield and other top-level keys. Never minify; avoid unnecessary reordering. -
Leave local-only skills untouched. Any skill in
localNamesbut not inglobalNamesmust remain in the file and on disk. Do not delete it. -
Never delete:
- The local lock file itself (even if
skillsbecomes empty — write{"version": <original>, "skills": {}}instead). - Any path outside
.agents/skills/(besides rewriting the lock file). - The global lock file or any global skill directories (global is read-only).
- The local lock file itself (even if
-
Idempotency. Running this command again must be a safe no-op: once a duplicated local skill is removed, subsequent runs simply find nothing in
toRemove. -
Report — produce the two-part report described in the Reporting section below, using the
localMtimescaptured in step 7 and thelocalGithubCommit/globalGithubCommitfrom step 8.
After execution, return: (A) a changes summary and (B) a per-touched-skill freshness table.
- Removed (name — was duplicated globally), with the local lock path that was rewritten.
- Kept local (name — not present globally).
- Counts of each, plus the resulting number of entries in the local lock file.
Use the GitHub commit data from step 8 for the authoritative comparison. The localMtimes / globalMtimes are shown alongside for informational purposes when both installs are at the same SHA.
For every skill in toRemove, render one row per skill:
| Skill | Local commit | Global commit | Local SKILL.md mtime | Global SKILL.md mtime | Status | |---|---|---|---|---|
Where:
- Local/Global commit:
sha (date)fromlocalGithubCommit[skill]/globalGithubCommit[skill]. Shorten SHA to 12 chars. Use"same"if both installs are at identical content (no SHA shown in that case). Use"unknown"if the GitHub API fetch failed. - Local/Global SKILL.md mtime: from
localMtimes[skill]/globalMtimes[skill]— shown for all skills. When SHA is"same", the mtime delta tells you which install ran first (the earlier mtime = installed first). This is informational only — identical SHA means identical content regardless of mtime. - Status:
globalSha === localSha→ 🟢safe— same source commit; the mtime columns show which was installed first (global with earlier mtime = installed first).globalGithubCommit[skill].date ≥ localGithubCommit[skill].date→ 🟢safe(global is at same or newer commit).globalGithubCommit[skill].date < localGithubCommit[skill].date→ 🔴 regression (global is at an older commit; removing local loses newer content).- Either SHA is
"error"or"unknown"→ ⚪unknown(could not determine).
Highlighting. For any row where globalGithubCommit[skill].date < localGithubCommit[skill].date, color the row red with <span style="color: red">…</span> and include both commit SHAs and dates plus the delta in the Status cell, e.g. 🔴 REGRESSION: global abc123 (2026-07-01) < local def456 (2026-08-05). If the host renders plain Markdown only, the 🔴 REGRESSION: prefix is the fallback signal.
After the table, if any row was flagged as a regression (🔴), add a prominent note: "npx skills add --scope global <name>) before relying on the global version."
If any row has "same" SHA, add an informational note: "ℹ️ N skill(s) have identical source commits but different SKILL.md mtimes, meaning they were installed at different times. The global copy (earlier mtime) was installed first; the local copy is a redundant duplicate of the same content."
SKILL.md mtime tracks when the file was extracted to disk, which follows installation order — not source repo freshness. In the bug this fixes: local and global were both at git commit 84fdeffd12f2 (identical content), but global was installed ~2 min before local, so global's SKILL.md mtime was ~2 min earlier than local's. An mtime-based gate would report a regression where none exists. The GitHub API commit date (commit.committer.date) is the only reliable freshness signal because it tracks when content actually changed in the source repo.
- Operate only on the local lock file (whichever variant was found) and
.agents/skills/<skill-name>/within the current repository, plus read-only access to the global lock and global skill directories. - Removal membership is by name key only (
toRemove = localNames ∩ globalNames). GitHub commit comparisons (step 9) gate the preflight check and report, but never block removal — the goal is de-duplication, not version pinning. - If a skill name appears locally but its directory is missing on disk, still remove the stale lock entry and note it in the report.
- If a directory exists under
.agents/skills/but has no lock entry, leave it alone (do not infer or invent lock entries). - If the step-9 majority-regression condition is met, stop before deletion and inform the user. Use the GitHub commit SHA comparison, not SKILL.md mtime — see the diagnostic note in the Reporting section for why.
- Do not touch
.git, dependencies, or any non-skill files.
- Use
os.path.expanduser('~')in Python oros.homedir()in Node.js to expand~. - $env:USERPROFILE may not be reliable for all users; prefer the above methods.
- $environment variables like
%USERPROFILE%may not be reliable for all users; prefer the above methods.
- Use
os.path.expanduser('~')in Python oros.homedir()in Node.js to expand~. - $HOME is usually reliable, but prefer the above methods for cross-platform consistency.