跳轉到

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 已經是完整的 inline strict: false skill bundle。

CLI 怎麼解析 manifest 路徑

使用者給 npx skills add 的引數會被解析成 repo + 可選的 subpath。 以這個 repo 為例:

npx skills@latest add daviddwlee84/agent-skills/skills
                       └────────── repo ──────────┘ └sub┘

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() 用一樣的方式排序。所以有兩個排序鍵,兩個都按字母、都不吃陣列順序:

  1. 分組順序 —— plugins[].name(kebab),A→Z。
  2. 分組內順序 —— 每個 skill 的 SKILL.md name,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-official
  • anthropic-marketplace、anthropic-plugins
  • agent-skills
  • knowledge-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 內。

依 src/skills.ts:

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 可以擴充成:

  1. 解析每個 SKILL.md 的 frontmatter(例如透過 yq 或 python-frontmatter)。
  2. 如果 plugins[].skills[] 中的任何路徑解析到帶 metadata.internal: true 的 skill 就 error。
  3. 對 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 後:

  1. 一如往常把 SKILL.md 放在 skills/local/<name>/ 或 skills/vendor/<name>/。
  2. 打開 skills/.claude-plugin/marketplace.json,把新路徑(相對於 skills/)加到對應 plugin 的 skills[] 陣列中,或者故意省略 讓它落到 "Other"。
  3. 跑 make marketplace。它會對沒列出的 skill 發 warning、對壞掉的 路徑發 error。

原始碼參考