How npx skills reads catalog metadata — npx skills 怎麼讀 catalog metadata¶
Terminology rule (zh-TW pages)
技術名詞首次出現以「中文 (English original)」格式呈現,例:依賴注入
(dependency injection)。不自創翻譯——若無公認譯名直接保留英文
(如 embedding、tokenizer)。代碼、API 名、CLI flag、套件名、檔名一律不翻。
這頁記錄 npx skills@latest add ... 分組安裝 UI 背後的已驗證機制。
以下內容全部由實際閱讀 npm package 原始碼(vercel-labs/skills,
src/plugin-manifest.ts
和 src/skills.ts)
以及官方
Claude Code plugin-marketplaces 文件
確認。
給這個 repo 的 TL;DR¶
- 你跑
npx skills@latest add daviddwlee84/agent-skills/skills時看到的 分組 picker UI 是由skills/.claude-plugin/marketplace.json驅動的 —— 在skills/目錄裡面,不是 repo 根目錄。 - 每個分組標頭 (group header) 是
kebabToTitle(plugins[].name)。 沒被列在任何 plugin 底下的 skill 會 fall through 到 Other 分組。 - 用手編輯 manifest;跑
make marketplace(=bash scripts/validate-marketplace.sh)抓出壞掉的路徑、重複條目、 或會被靜默歸到 "Other" 的 on-disk skill。 - 我們不出貨每個 plugin 各自的
.claude-plugin/plugin.json;每個 marketplace entry 已經是完整的 inlinestrict: falseskill bundle。
CLI 怎麼解析 manifest 路徑¶
使用者給 npx skills add 的引數會被解析成 repo + 可選的 subpath。
以這個 repo 為例:
CLI 內部
(src/skills.ts):
const searchPath = subpath ? join(basePath, subpath) : basePath;
// ...
const pluginGroupings = await getPluginGroupings(searchPath);
所以當安裝指令包含 subpath 時,CLI 會從
<repo>/<subpath>/.claude-plugin/marketplace.json 讀 catalog。
| 調用 | CLI 找 manifest 的位置 |
|---|---|
npx skills add anthropics/skills |
<repo>/.claude-plugin/marketplace.json |
npx skills add daviddwlee84/agent-skills/skills |
<repo>/skills/.claude-plugin/marketplace.json |
npx skills add foo/bar/some/dir |
<repo>/some/dir/.claude-plugin/marketplace.json |
這個 repo 用第二種形式,因為 skills/local/ 跟 skills/vendor/ 樹住在
skills/ 底下,不是 repo 根目錄。
常見陷阱 (Common pitfall)
把 .claude-plugin/marketplace.json 放在 repo 根目錄、
然後用帶 subpath 的指令調用 CLI,會靜默地停用分組 ——
根目錄的檔案永遠不會被讀,每個 skill 都會落到 Other。
讓 manifest 的位置匹配安裝指令的 subpath。
讓 Claude Code 與 Codex 共用 nested manifest¶
同一個檔案由三種 installer 消費,但各自獨立決定 marketplace root:
| Consumer | Marketplace root |
|---|---|
npx skills add daviddwlee84/agent-skills/skills |
指定的 skills/ subpath |
claude plugin marketplace add owner/repo |
clone 後的 repo 根目錄 |
claude plugin marketplace add ./local/path |
指令指定的 local path |
codex plugin marketplace add ./local/path |
指令指定的 local path |
因此 local checkout 可以零重複地提供兩條原生 plugin 路徑:
git clone https://github.com/daviddwlee84/agent-skills.git
claude plugin marketplace add ./agent-skills/skills
claude plugin install version-control@daviddwlee84-skills
codex plugin marketplace add ./agent-skills/skills
codex plugin add version-control@daviddwlee84-skills
已用 Claude Code 2.1.235 與 Codex 0.147.0 驗證。Codex 會讀 inline 的
Claude-format marketplace entry,並在 install cache 裡生成
.codex-plugin/plugin.json adapter;其中只保留該 category 明列的
skills[]。repo 不需要維護第二份 source manifest。
repo 根目錄沒有支援的 marketplace manifest,所以必須傳入其 skills/
目錄。因此 Claude 的 GitHub shorthand claude plugin marketplace add
daviddwlee84/agent-skills 會失敗。直接給 nested JSON URL 也不是等價的
Claude workaround:URL marketplace 只取得 catalog 檔,旁邊沒有 repo tree,
相對的 source: "./" 與 skill path 就沒有可安裝的 source。
兩條原生路徑的安裝單位都是 category plugin;npx skills 則能跨 agent
安裝單一指定 skill。make native-marketplace-smoke 會在 temporary config
state 中實際跑兩條 local native 路徑,並驗證 runtime adapter 沒有跨
category 洩漏。
Manifest 形狀¶
參考:官方 marketplace manifest schema。
{
"name": "<unique-marketplace-id>",
"owner": { "name": "...", "email": "..." },
"metadata": {
"description": "...",
"version": "1.0.0"
},
"plugins": [
{
"name": "<group-name-kebab-case>",
"description": "Short description of the plugin / category",
"category": "<for Claude Code /plugin UI>",
"tags": ["...", "..."],
"source": "./",
"strict": false,
"skills": [
"./relative/path/to/skill-dir",
"./another/skill-dir"
]
}
]
}
npx skills picker 實際上消費的:
| 欄位 | 給 npx skills 用? |
給 Claude Code /plugin UI 用? |
|---|---|---|
metadata.pluginRoot |
yes (解析 source) |
yes |
plugins[].name |
yes — 驅動分組標頭文字 | yes |
plugins[].source |
yes (必須是以 ./ 開頭的字串;object/remote source 會被跳過) |
yes |
plugins[].skills[] |
yes (必須以 ./ 開頭) |
yes |
plugins[].description / category / tags / version / strict |
no — pass through | yes |
所以 category 跟 tags 目前算是免費的 metadata:放進 manifest
不花成本,未來任何原生消費者(Claude Code 的 /plugin 瀏覽器、
下游 catalog 工具)可以用它們,我們不用搬移檔案。
picker 對每個 skill 列實際顯示什麼¶
依
src/add.ts
(groupMultiselect 呼叫),每個 skill 列被建構為:
{
value: s,
label: getSkillDisplayName(s), // = s.name (SKILL.md frontmatter)
hint: s.description.slice(0, 57)+'…' // = SKILL.md description, truncated to 60 chars
}
所以使用者每個 skill 看到的只有 SKILL.md 的 name + 截斷的
description。marketplace.json 中沒有任何東西會 per-skill 顯示
—— 不是 plugins[].description,不是 category,不是 tags,什麼都不是。
plugin 的 name 只會以分組標頭出現在 skill 列上方。
意思是:如果你想對 picker 中單一 skill 加註解 / 改標籤 /「標為棄用」/ 覆寫 description,SKILL.md 本身是唯一的旋鈕 (knob)。catalog manifest 中沒有「外部註解」機制。
分組標頭渲染 —— kebabToTitle¶
picker 把每個 plugin name 用連字號 (hyphen) 切開、每個 token 做
titlecase,組成標頭文字。所以:
plugins[].name |
UI 標頭 |
|---|---|
document-skills |
Document Skills |
claude-api |
Claude Api |
ml-workflow |
Ml Workflow |
fullstack-nextjs |
Fullstack Nextjs |
notebooks |
Notebooks |
挑 titlecase 起來能讀的名字。claude-api → "Claude Api" 是官方
upstream 的選擇,現役在 anthropics/skills 的使用者介面中出現,
所以彆扭的 casing 是已知怪癖;不要用奇怪的 name 欄位來繞過。
picker 的排序(只按字母)¶
picker 純粹按字母排序 —— marketplace.json 中 plugins[] 陣列的順序、
以及每個分組 skills[] 陣列的順序,都會被忽略。已在
src/add.ts
確認(互動 groupMultiselect 路徑,約 L1298):
const sortedSkills = [...skills].sort((a, b) => {
if (a.pluginName && b.pluginName && a.pluginName !== b.pluginName)
return a.pluginName.localeCompare(b.pluginName); // ① 分組名,A→Z
return getSkillDisplayName(a).localeCompare(getSkillDisplayName(b)); // ② skill 名,A→Z
});
非互動的 list / summary / results 路徑也透過 Object.keys(grouped).sort()
用一樣的方式排序。所以有兩個排序鍵,兩個都按字母、都不吃陣列順序:
- 分組順序 ——
plugins[].name(kebab),A→Z。 - 分組內順序 —— 每個 skill 的
SKILL.mdname,A→Z。
唯一的槓桿:改分組名讓它排在前面¶
因為排序鍵就是分組 name 字串本身,唯一能把分組往上移的方法就是讓它的
名字排得更前面。localeCompare 把數字排在字母前面,所以兩位數 NN-
前綴可以用明確順序把分組釘在最上面(前綴會透過 kebabToTitle 出現在
標頭 —— 這是可接受的取捨):
plugins[].name |
picker 標頭 | 位置 |
|---|---|---|
01-project-memory |
01 Project Memory |
第 1 |
02-skill-authoring |
02 Skill Authoring |
第 2 |
03-infra-and-docs |
03 Infra And Docs |
第 3 |
04-ml-workflow |
04 Ml Workflow |
第 4 |
05-notebooks |
05 Notebooks |
第 5 |
| (其餘全部) | (kebabToTitle) | 所有 NN- 分組之後,A→Z |
NN- 前綴是刻意的,不是 typo
拿掉前綴會讓那個分組掉回它的 A→Z 位置。這個 repo 用這招把常用分組
釘在上面;其餘維持字母序。任何改名後重跑 make marketplace。
分組內順序無法控制,除非改 skill 本身的名字 —— 而 SKILL.md name
是有實質作用的(它是 install id),所以別改。例:02 Skill Authoring
底下的列會渲染成 mcp-builder → skill-author → skill-creator,跟
skills[] 陣列順序無關。
"Other" —— 自動 fallback 分組¶
CLI 在搜尋根目錄底下發現的 SKILL.md,只要不在任何
plugins[].skills[] 條目中,都會出現在 Other 分組標頭底下。
這就是為什麼 anthropics/skills 的 template-skill 落在 "Other"
—— 因為它沒被列在他們 manifest 的 plugins[]。
利用這點:暫時不適合任何分類的 skill,可以單純從 marketplace.json
省略,它仍然會被安裝,只是落在 "Other"。我們的 make marketplace
validator 對沒列出的 on-disk skill 發 warning(不是 error),
這樣容易注意到。
路徑解析規則(陷阱)¶
source: "./..."跟skills[]: "./..."是相對於 marketplace 根目錄 (含.claude-plugin/的目錄)的路徑,不是相對於 JSON 檔案本身或.claude-plugin/目錄。- CLI 拒絕任何不以
./開頭的路徑 ——../跟絕對路徑都不允許 (路徑遍歷保護 (path-traversal protection) 在isContainedIn)。 metadata.pluginRoot(可選)會被前置 (prepend) 到每個 plugin 的source。我們沒用它;我們在每個 plugin 設source: "./",這樣 每個 plugin 路徑直接解析在 manifest 根目錄底下 (skills/)。
保留的 marketplace name¶
依官方文件,這些不能用作 name 欄位:
claude-code-marketplace、claude-code-plugins、claude-plugins-officialanthropic-marketplace、anthropic-pluginsagent-skillsknowledge-work-plugins、life-sciences- 任何冒充官方 marketplace 的名字
(例如
official-claude-plugins、anthropic-tools-v2)
這個 repo 的 manifest 使用 daviddwlee84-skills —— GitHub repo
資料夾仍然可以叫 agent-skills;只有 manifest 內的 name 欄位被限制。
validator script 會強制這條。
Versioning¶
Top-level metadata.version 描述 marketplace catalog;它不會 pin 這些
inline plugin。Claude Code 2.1.235 的 plugin version identity 優先順序是
plugin.json、個別 plugins[].version、最後才是 git source SHA。這些 entry
沒有前兩者,所以隔離更新測試中,即使 metadata.version 一直保持
"1.0.0",新 commit 後安裝的 SHA 與內容仍會前進;省略欄位的行為相同。
因此這個 repo 保留 metadata.version: "1.0.0" 作為 catalog metadata,
不是 release gate。git-backed install 仍要走正常 refresh:取得新 source
(文件中的 local checkout 先跑 git pull)、執行 claude plugin
marketplace update daviddwlee84-skills,再執行 claude plugin update
<plugin>@daviddwlee84-skills。npx skills 分組路徑不使用這個欄位。
marketplace.json vs plugin.json¶
| 檔案 | 目的 | 何時使用 |
|---|---|---|
.claude-plugin/marketplace.json |
一個或多個 plugin / 分類的 catalog | 讓使用者註冊成 marketplace 的 repo |
<plugin>/.claude-plugin/plugin.json |
一個 plugin 的 manifest | 擁有自己 component tree 的 conventional strict marketplace plugin |
兩個檔案不是冗餘的,但 marketplace entry 可以 inline 定義 manifest-less
bundle。這個 repo 正是如此:每個 entry 設 strict: false、讓 source
指向共用的 skills/ root,並明列其 skill 目錄。加入一批近乎空白的
plugin.json 不會讓 discovery 更正確,只會多一個要同步的 identity / version
surface。
Cross-agent portability¶
npx skills 是個單向安裝器:它讀 manifest,然後把 SKILL.md 檔案
複製到每個目標 agent 的原生 skills 目錄(支援的目標列在
src/types.ts)。
大多數其他 agent(OpenCode、Cursor、Aider、…)不會原生讀
.claude-plugin/marketplace.json;它們有自己的慣例(.opencode/、
AGENTS.md、.cursor/rules/ 等)。Codex 0.147.0 是已驗證的例外:
它的 plugin marketplace CLI 會直接接受這份 manifest。
因此 distribution path 是:
- 一個
marketplace.json足以透過npx skills add做 cross-agent install。 - 傳入
skills/作為 local marketplace root 時,同一份檔案也提供 Claude Code 與 Codex 的 category-plugin 安裝。 - 安裝在
.agents/skills/的 standalone Codex skill 繼續使用 canonical shared discovery path,不需要 plugin packaging。
執行 codex plugin add 時,Codex 會複製共用 source,並在 cache 中生成
.codex-plugin/plugin.json;其 skills[] 只包含選定 category。不要為了
重製這個 adapter 而 commit 第二份 manifest。只有當這個 repo 日後選擇
發布帶 richer interface metadata 的獨立 Codex-specific package 時,source
.codex-plugin/plugin.json 才有意義。Codex public CLI 不安裝任意 .plugin
或 ZIP archive,所以 packaging script 仍需要明確的 release / upload consumer。
不刪除而是隱藏 / 棄用 (deprecate) 一個 skill¶
CLI 透過 SKILL.md frontmatter 內建一個隱藏機制 —— 設
metadata.internal: true,skill 在 picker 中變不可見,但留在 repo 內。
const isInternal = data.metadata?.internal === true;
if (isInternal && !shouldInstallInternalSkills() && !options?.includeInternal) {
return null;
}
---
name: my-deprecated-skill
description: ...
metadata:
internal: true # <- 把這個 skill 從 `npx skills add` picker UI 隱藏
---
這做的事:
- ✅ Skill 留在 repo,檔案不變。
- ✅ 預設從互動 picker 隱藏。
- ✅ 直接請求仍可安裝:
npx skills add <repo> my-deprecated-skill傳includeInternal: true所以按名字查找會找到它。 - ✅ 進階使用者覆寫:
INSTALL_INTERNAL_SKILLS=1 npx skills add ...也會把 internal skill 顯示在 picker 中。 - ✅ 與其他地方的發現相容 (Claude Code 的自動發現、這個 repo 的
docs 站) ——
metadata.internal只是npx skills慣例。
把 skill 標為 internal 時,也要從 marketplace.json 的
plugins[].skills[] 移除它的路徑 —— 不然你會有指向一個隱藏 skill
的死 catalog 條目。同一個 skill 不應該同時有這兩種設定。
為什麼不直接刪?¶
用 metadata.internal 而不是刪除的理由:
- 保留 docs 頁面可達 (skill 仍會在 docs 站渲染)。
- 為 vendored skill 保留
vendor.yaml歷史與last_sync日期。 - 在棄用寬限期 (deprecation grace period) 允許按名字 opt-in 安裝。
- 為未來 debug 保留
git log/git blame連續性。
如果你想要永久消失,刪除目錄、從 marketplace.json 移除條目、
(vendored skill 的話)也從 vendor.yaml 移除。
Validator 涵蓋範圍(今日)¶
scripts/validate-marketplace.sh 目前不解析 SKILL.md frontmatter,
所以它不會抓出「internal skill 被列在 marketplace.json」這種錯誤。
如果我們在實務上撞到這個問題,validator 可以擴充成:
- 解析每個 SKILL.md 的 frontmatter(例如透過
yq或 python-frontmatter)。 - 如果
plugins[].skills[]中的任何路徑解析到帶metadata.internal: true的 skill 就 error。 - 對 internal skill 跳過「fall under Other」的 warning(它們根本不會 在 picker 出現,所以 warning 是誤導的)。
這是個延期的增強 —— 看 TODO,當變得值得做時再說。
在這個 repo 裡操作 manifest¶
# 驗證 manifest(解析、name 沒被保留、所有路徑存在、
# 沒重複、on-disk skill 都有覆蓋)。
make marketplace
# 或直接跑 script 取得 verbose 輸出。
bash scripts/validate-marketplace.sh
# 在 temporary config state 實跑 Claude + Codex 原生 add/list/install。
make native-marketplace-smoke
加入新的 local 或 vendored skill 後:
- 一如往常把 SKILL.md 放在
skills/local/<name>/或skills/vendor/<name>/。 - 打開
skills/.claude-plugin/marketplace.json,把新路徑(相對於skills/)加到對應 plugin 的skills[]陣列中,或者故意省略 讓它落到 "Other"。 - 跑
make marketplace。它會對沒列出的 skill 發 warning、對壞掉的 路徑發 error。
原始碼參考¶
npx skillspackage 原始碼: vercel-labs/skills on GitHub, npm packageskills。- Manifest 讀取邏輯:
src/plugin-manifest.ts。 - Subpath + searchPath 邏輯:
src/skills.ts。 - Picker UI 分組:
src/add.ts(搜尋groupMultiselect與kebabToTitle)。 - 官方 Claude Code 文件: plugin-marketplaces、 plugins-reference。
- 這個 repo 的參考 manifest:
skills/.claude-plugin/marketplace.json。