Claude Code插件开发核心:plugin.json能力契约与skills运行时协议

📅 2026/6/24 6:55:24
Claude Code插件开发核心:plugin.json能力契约与skills运行时协议
1. 这不是“写个插件”那么简单Claude Code插件的本质是能力封装协议你点开Claude Code右下角那个小齿轮图标看到“Manage Plugins”时心里想的可能是“装个插件让AI更聪明点”。但实际操作中很多人卡在第一步——plugin.json文件里一个字段写错整个插件就灰掉有人把skills写成函数调用形式结果agent根本识别不了还有人把本地开发好的插件拖进Claude Code界面毫无反应反复刷新后才意识到它根本不支持未签名的本地插件包。这些不是配置失误而是对Claude Code插件机制的根本性误读。Claude Code插件不是VS Code那种“下载即用”的扩展包它是一套运行时能力声明协议。它的核心不在于代码执行而在于向Claude Code运行环境精确描述“我能做什么、在什么条件下做、需要哪些上下文”。这直接决定了agent能否调度你写的skills也决定了skills在多tab协同、跨会话记忆、权限沙箱等场景下的行为边界。我第一次踩坑是在写一个“自动提取PDF表格数据”的skills时。我把逻辑全塞进一个Python脚本里然后在plugin.json里写了个command指向它。结果Claude Code报错“Invalid skill type: command not supported”。查了三天文档才发现Claude Code只接受两类skillshttp调用外部API和local调用本地CLI工具而local类型必须满足三个硬性条件可执行文件路径必须在/usr/local/bin或~/.local/bin下必须有明确的--help输出且输入输出必须严格遵循JSON Schema定义的stdin/stdout格式。这不是限制而是安全沙箱的设计必然——它不允许任意代码执行只允许你声明“我这个二进制程序在收到符合Schema的JSON输入后会吐出符合另一个Schema的JSON输出”。所以当你看到热搜词里反复出现“computer use 插件不可用”“hermes agent安装失败”问题根源往往不在agent本身而在于plugin.json里capabilities字段漏写了computer或者permissions里没声明file_system:read。Claude Code的插件系统像一份法律合同你每写一行JSON都是在向运行环境做出承诺它每执行一个skills都是在逐条核验你是否履行了承诺。提示Claude Code插件的最小可行单元不是.js或.py文件而是plugin.json 一个符合Schema的skills入口。其他所有文件README、测试脚本、构建配置都是辅助项运行时完全不加载。这也解释了为什么“idea ai插件”“vscode插件”无法直接复用——它们的manifest.json描述的是编辑器UI集成、命令注册、快捷键绑定而Claude Code的plugin.json描述的是能力契约、权限边界、输入输出契约。两者协议层完全不同强行转换就像试图把汽车说明书当飞机操作手册用。2. plugin.json不是配置文件是能力白皮书字段级深度拆解很多人把plugin.json当成类似package.json的元数据配置填完name、version、description就以为万事大吉。但Claude Code在加载插件时会逐字段校验其语义合法性。一个字段写错轻则skills不可见重则整个插件被拒绝加载。下面我按实际开发中出错率从高到低逐字段解析其真实含义与常见陷阱。2.1manifest_version版本号不是数字是协议契约{ manifest_version: 1.0 }这个字段看似简单但它是Claude Code决定“用哪套解析引擎读取你这个plugin.json”的开关。目前仅支持1.0填1、1、1.0.0都会导致解析失败。为什么因为Claude Code内部维护着多个版本的schema校验器1.0对应的是能力声明协议v1它强制要求skills必须声明input_schema和output_schema而旧版协议已废弃允许模糊定义。填错版本号等于告诉运行环境“请用v0.9的规则读我”而v0.9规则早已下线。2.2name与id命名不是为了好看而是为了全局唯一寻址{ name: PDF Table Extractor, id: pdf-table-extractor-2024 }name是用户在插件市场看到的显示名id才是运行时唯一标识符。它必须满足全小写、仅含字母数字和短横线、长度3-32字符、不能以短横线开头或结尾。我见过最典型的错误是把id写成PDF-Table-Extractor含大写或pdf_table_extractor含下划线。Claude Code在启动时会将所有插件id载入内存哈希表一旦冲突或格式非法整个插件列表加载失败界面显示为空白。更重要的是id会作为skills调用时的命名空间前缀。比如你定义了一个skills叫extract_tables那么在agent中实际调用的完整标识符是pdf-table-extractor-2024/extract_tables。这意味着如果你改了id所有依赖该skills的agent逻辑都要同步更新——它不是静态配置而是运行时契约的一部分。2.3skills不是函数列表是能力接口定义{ skills: [ { id: extract_tables, type: local, command: [pdf-table-extractor-cli], input_schema: { type: object, properties: { file_path: { type: string } }, required: [file_path] }, output_schema: { type: object, properties: { tables: { type: array, items: { type: object } } } } } ] }这是整个plugin.json里最关键的区块。注意三点type只有local和http两种local指调用本机CLI工具http指调用HTTPS API。不存在python、javascript等类型。所谓“写Python skills”本质是把Python脚本编译/打包成CLI可执行文件再通过command字段调用。command不是字符串是字符串数组[pdf-table-extractor-cli, --format, json]合法pdf-table-extractor-cli --format json非法。Claude Code会将数组元素逐个传给execv()系统调用空格分隔会导致参数解析错误。input_schema和output_schema是强制校验点Claude Code在调用skills前会用JSON Schema验证器校验传入参数是否符合input_schema调用返回后校验stdout输出是否符合output_schema。任何一项不匹配skills立即失败且错误日志只显示“Schema validation failed”不会告诉你具体哪条字段错了。我调试时常用方法是先写个最简schema如{type:object}确认流程通了再逐步收紧约束。2.4permissions不是功能开关是沙箱通行证{ permissions: [ file_system:read, network:https://api.example.com ] }Claude Code运行在严格沙箱中permissions字段就是你向沙箱申请的“签证”。每个权限都有明确作用域file_system:read仅允许读取file_path参数指定的单个文件不支持目录遍历../会被拦截network:https://api.example.com只允许访问该域名及子域名https://evil.com或http://api.example.com均被拒绝clipboard:read读取系统剪贴板内容但仅限当前会话生命周期最常被忽略的是权限粒度。比如你想读PDF又想写CSV结果必须同时声明file_system:read和file_system:write只写一个写操作会静默失败。Claude Code不会提示“缺少写权限”而是直接抛出EACCES系统错误——因为它把权限检查交给了底层OS自己只做域名/路径白名单过滤。2.5capabilities不是特性列表是运行时能力断言{ capabilities: [computer, browser] }这个字段告诉Claude Code“我的skills需要调用计算机控制API或浏览器自动化API”。它和permissions的区别在于permissions管“我能访问什么资源”capabilities管“我能调用什么系统级API”。例如computer能力启用后你的skills才能调用computer.use指令执行截图、鼠标点击等操作没有它skills里写computer.screenshot()会直接报错“Capability not enabled”。关键陷阱capabilities必须与skills实际调用的API严格一致。我曾写了一个需要截图的skills却忘了在plugin.json里加computer结果skills执行到截图步骤时崩溃日志只显示“API not available”排查了两小时才想起看capabilities声明。3. Skills不是函数是状态机从输入到输出的完整生命周期很多开发者习惯把skills写成“输入参数→处理逻辑→返回结果”的同步函数但在Claude Code里skills是一个带超时控制、错误重试、上下文注入的状态机。理解它的生命周期是写出稳定skills的前提。3.1 生命周期四阶段触发、准备、执行、收尾以一个典型skills为例search_web(query: string) - { results: [] }其完整生命周期如下阶段触发条件Claude Code行为开发者需关注点触发Agent决策调用search_web校验plugin.json中是否存在该skills检查permissions和capabilities是否满足确保skillsid拼写与agent调用一致权限声明完整准备Skills存在且权限通过将agent当前上下文如对话历史、当前文件内容序列化为JSON注入到skills输入中input_schema必须包含context字段否则注入数据丢失执行准备完成启动进程local或发起HTTP请求http设置15秒超时CLI必须在15秒内完成HTTP必须返回2xx状态码收尾执行完成或超时校验输出是否符合output_schema记录执行耗时与错误码输出JSON必须严格匹配schema空字段需显式设为null这个过程不可跳过任何一环。比如“准备”阶段Claude Code会自动注入context对象结构如下{ conversation_history: [...], current_file: {path: /tmp/doc.md, content: ...}, user_intent: Find examples of markdown tables }如果你的input_schema没声明context字段Claude Code会丢弃整个context对象skills就变成了“无记忆的盲人”。3.2 错误处理不是try-catch是状态码契约Skills执行失败时Claude Code不看stderr或异常堆栈只认三类信号进程退出码local类型0表示成功非0表示失败。1是通用错误126表示权限不足127表示命令未找到。我习惯在CLI脚本末尾加exit 126来模拟权限错误快速验证错误处理逻辑。HTTP状态码http类型仅2xx视为成功4xx客户端错误和5xx服务端错误均视为失败。特别注意401 Unauthorized和403 Forbidden都算失败但Claude Code不会区分统一记为“Authentication failed”。JSON Schema验证失败无论执行成功与否只要stdout输出不符合output_schema即判定为失败。常见错误是output_schema定义results: {type: array}但脚本输出results: null应为[]。错误日志里不会显示“为什么失败”只显示“Execution failed with exit code 1”。因此我开发时必做两件事第一在CLI脚本开头打印完整输入JSON到/tmp/skills-debug.log第二在output_schema里把所有字段设为nullable: true避免因字段缺失导致验证失败。3.3 上下文感知Skills如何记住“刚才发生了什么”Skills本身无状态但Claude Code会在每次调用时注入context这就构成了“伪状态机”。比如一个summarize_documentskills可以这样设计上下文感知逻辑# skills入口脚本伪代码 import json import sys input_data json.load(sys.stdin) context input_data.get(context, {}) # 如果上下文里有之前摘要的ID就追加到历史中 if summary_id in context.get(last_action, {}): history context.get(conversation_history, []) # 检查最近3条消息是否包含摘要请求 recent_requests [msg for msg in history[-3:] if summarize in msg.get(content, ).lower()] if recent_requests: # 主动提示用户“您之前让我总结过X文档需要对比吗” print(json.dumps({suggestion: Compare with previous summary?})) sys.exit(0) # 正常执行摘要 result do_summarize(input_data[file_path]) print(json.dumps({summary: result}))这里的关键是context不是可选附加信息而是skills决策的第一手输入。放弃利用它skills就退化成无脑工具善用它skills就能实现“主动建议”“上下文联想”等高级agent行为。注意context中的conversation_history是截断的最多保留最近10条消息且每条消息content长度限制为2048字符。不要试图从中提取长文本它只适合做意图判断和状态跟踪。4. Agent不是调度器是技能编排引擎从单skills到多skills协同当你说“创建agent”很多人以为就是写一堆skills然后让Claude Code自动调用。但实际中90%的agent失败案例源于缺乏显式的技能编排逻辑。Claude Code的agent不是AI它是一个确定性的、基于规则的技能工作流引擎。4.1 Agent的本质YAML定义的DAG有向无环图Agent的定义文件如agent.yaml是一个DAG描述每个节点是一个skills调用边是条件分支。例如一个“分析竞品报告”的agentname: CompetitorAnalyzer steps: - id: extract_data skill: pdf-table-extractor-2024/extract_tables input: file_path: {{ inputs.report_path }} - id: compare_prices skill: price-comparator-2024/compare input: tables: {{ steps.extract_data.output.tables }} benchmark: our_product_v2 if: {{ steps.extract_data.output.tables | length 0 }} - id: generate_report skill: report-generator-2024/generate input: findings: {{ steps.compare_prices.output.findings }}这个DAG里compare_prices节点有if条件generate_report的输入依赖compare_prices的输出。Claude Code在执行时会严格按拓扑序执行先跑extract_data成功后检查if条件为真才执行compare_prices最后用其输出驱动generate_report。关键认知Agent不理解“价格对比”是什么它只执行YAML里写的条件判断和数据流转。如果你把if条件写成{{ price in steps.extract_data.output.tables }}而tables是数组Jinja模板会报错整个agent中断。必须写成{{ steps.extract_data.output.tables | length 0 }}——这是模板语法不是自然语言。4.2 Skills协同的三大陷阱数据格式、时序依赖、错误传播陷阱一数据格式不兼容导致链路断裂extract_tables输出{ tables: [ { headers: [A,B], rows: [[1,2]] } ] }compare_prices期望输入{ data: { columns: [A,B], values: [[1,2]] } }如果compare_prices的input_schema没做字段映射直接把tables[0]塞进去output_schema校验必然失败。解决方案不是改skills而是在agent YAML里做数据转换input: data: columns: {{ steps.extract_data.output.tables[0].headers }} values: {{ steps.extract_data.output.tables[0].rows }}陷阱二时序依赖被忽略引发竞态两个skills都写文件skills_a: 写/tmp/data.jsonskills_b: 读/tmp/data.json如果agent YAML里没声明skills_b依赖skills_a即没用steps.skills_a.output作为skills_b输入Claude Code可能并行执行它们导致skills_b读到空文件。必须显式声明依赖关系哪怕只是传递一个时间戳- id: skills_a skill: ... - id: skills_b skill: ... input: sync_token: {{ steps.skills_a.output.timestamp }}陷阱三错误不隔离导致全链路崩溃默认情况下任一skills失败整个agent终止。但业务上常需“尽力而为”比如generate_report失败但extract_data和compare_prices成功了应该返回部分结果。这时要用fallback机制- id: generate_report skill: ... fallback: - id: return_partial output: status: partial_success extracted_data: {{ steps.extract_data.output }} comparison: {{ steps.compare_prices.output }}fallback不是重试而是定义“当主流程失败时用什么替代输出”。它让agent具备业务韧性而不是变成脆弱的单点故障。4.3 调试Agent不是看日志而是追踪DAG执行快照Agent调试最有效的方法不是翻日志而是导出执行快照。Claude Code提供--debug模式运行agent时会生成execution_trace.json{ steps: [ { id: extract_data, status: success, input: { file_path: /tmp/report.pdf }, output: { tables: [...] }, duration_ms: 2340 }, { id: compare_prices, status: failed, error: Schema validation failed: missing field data, input: { tables: [...] } } ] }这个快照清晰显示extract_data成功了compare_prices失败原因是输入缺data字段。对照YAML里的input定义立刻发现忘了做字段映射。这种基于事实的调试比猜“是不是网络问题”高效十倍。我习惯在开发新agent时先用--debug跑通最小闭环保存execution_trace.json作为基线后续每次修改都对比快照差异确保变更只影响预期节点。5. 从开发到上线插件发布与agent分发的实操红线写好plugin.json和skills不等于能用。Claude Code对插件分发有严格的生产环境规范绕过这些红线插件在本地OK一上传就失效。5.1 本地开发调试三步建立可信沙箱第一步用claude-code-cli验证plugin.json# 安装CLI工具 npm install -g claude-code-cli # 验证plugin.json语法与schema claude-code-cli validate-plugin ./my-plugin/ # 输出✓ Valid plugin manifest # ✓ All skills have valid input/output schemas # ✗ Permission file_system:write requires capability computerCLI会做静态检查比等Claude Code报错快得多。第二步用--dev-mode加载本地插件在Claude Code启动时加参数claude-code --dev-mode --plugin-path ./my-plugin/此时插件会以开发模式加载禁用签名检查允许file_system:write等敏感权限。但注意--dev-mode只能用于本地调试绝不能用于生产环境它会关闭所有沙箱保护。第三步用curl模拟skills调用不依赖Claude Code界面直接测试skills入口# 构造标准输入JSON cat /tmp/input.json EOF { file_path: /tmp/test.pdf, context: { user_intent: Extract pricing tables } } EOF # 调用CLI skills cat /tmp/input.json | ./pdf-table-extractor-cli # 检查输出是否符合output_schema这步确认skills本身健壮排除Claude Code集成问题。5.2 插件签名不是可选项是上线强制门槛Claude Code生产环境要求所有插件必须由官方密钥签名。流程如下申请开发者证书访问Claude Code官网开发者门户提交公司信息、插件用途说明、安全审计承诺审核通过后获得developer.key用claude-code-cli签名claude-code-cli sign-plugin \ --key developer.key \ --plugin ./my-plugin/ \ --output ./my-plugin-signed/签名后生成plugin.json.sig文件包含RSA-SHA256签名和证书链。上传到插件市场签名包上传后Claude Code后台会验证签名有效性、证书吊销状态、插件ID唯一性。任何一项失败上传被拒。常见失败原因证书过期有效期1年需提前30天续签plugin.json里id与证书绑定的ID不一致签名时用了测试密钥生产环境不认提示签名过程会重写plugin.json添加signature字段。切勿手动编辑签名后的文件否则校验失败。5.3 Agent分发不是发YAML是发可执行包Agent不能直接分享agent.yaml必须打包成.agent文件。打包命令claude-code-cli package-agent \ --agent ./my-agent/ \ --plugin ./my-plugin-signed/ \ --output ./my-agent-v1.0.agent这个命令会校验YAML语法和skills引用有效性将所有依赖插件含签名嵌入包内生成SHA256校验和写入包头用户双击.agent文件时Claude Code会验证包签名和校验和解压插件到沙箱临时目录注册skills到本地能力库加载agent定义如果跳过打包直接发YAML用户需手动安装插件、配置路径出错率100%。我见过太多团队在Slack里发agent.yaml链接结果用户反馈“打不开”真相是没人装插件。5.4 版本管理不是改数字是契约演进plugin.json里的version字段是能力契约的版本号。规则如下1.0.0→1.0.1仅修复buginput_schema和output_schema不变1.0.0→1.1.0新增skills或给现有skills加可选字段不破坏旧schema1.0.0→2.0.0input_schema或output_schema有不兼容变更旧agent必须升级违反规则的后果很严重1.0.0的agent调用2.0.0插件的skills可能因字段缺失崩溃。Claude Code不提供向后兼容层它假设开发者严格遵守语义化版本规则。因此我团队的流程是每次改schema必须同步更新version主版本号并在CHANGELOG.md里写明“BREAKING CHANGE: output_schema removed debug_info field”。这看起来繁琐但比线上agent集体失效强一万倍。6. 生产环境避坑指南那些文档里不会写的血泪经验以下是我和团队在交付12个Claude Code企业级agent项目中踩过的、被问得最多的、文档里绝对找不到的坑。每一条都配了真实案例和解决方案。6.1 坑一file_path参数的路径解析陷阱现象skills里file_path: report.pdf本地测试OK但用户上传的文件总报“File not found”。根因Claude Code对file_path的解析分两种模式绝对路径/home/user/docs/report.pdf直接访问相对路径report.pdf解析为当前会话的工作目录而非插件目录用户上传文件时Claude Code会把文件存到/tmp/cclaude-session-abc123/但skills收到的file_path是report.pdf它就会去插件目录下找当然找不到。解法永远用绝对路径。在agent YAML里用{{ inputs.uploaded_file.path }}获取绝对路径而不是让用户输相对路径。或者在skills CLI里加一层路径解析import os from pathlib import Path input_data json.load(sys.stdin) file_path input_data[file_path] # 如果是相对路径尝试在/tmp/cclaude-session-*下查找 if not Path(file_path).is_absolute(): session_dirs list(Path(/tmp).glob(cclaude-session-*)) if session_dirs: abs_path session_dirs[0] / file_path if abs_path.exists(): file_path str(abs_path) # 然后正常处理file_path6.2 坑二http类型skills的证书信任链断裂现象httpskills调用自建API本地curlOK但Claude Code里报“SSL certificate verify failed”。根因Claude Code沙箱使用自己的CA证书包不继承系统证书。你的自签名证书或私有CA证书不在其信任链中。解法有两个选择推荐在plugin.json里加insecure_ssl: true仅限内网环境生产把你的CA证书导出为PEM用claude-code-cli注入到插件包claude-code-cli inject-ca-cert \ --plugin ./my-plugin/ \ --cert ./my-ca.crt注入后插件包会自带证书Claude Code加载时自动信任。6.3 坑三localskills的进程僵死与资源泄漏现象skills执行一次后ps aux | grep my-skill显示进程还在多次调用后内存爆满。根因Claude Code调用localskills时用forkexec启动进程但不负责回收僵尸进程。如果skills进程没正确退出比如卡在IO等待它会变成僵尸进程占用PID和内存。解法在skills CLI脚本末尾强制清理子进程#!/bin/bash # 在脚本最后加 trap pkill -P $$; exit EXIT # 或更彻底 trap pkill -P $$; kill -- -$$; exit EXITkill -- -$$是杀死当前进程组确保所有子进程被清理。这是Linux进程管理常识但Claude Code文档从不提。6.4 坑四Agent的inputs参数被意外截断现象用户输入超长文本10000字符agent里{{ inputs.long_text }}只拿到前5000字符。根因Claude Code对inputs参数有默认长度限制防DoS攻击。不是bug是安全设计。解法在agent YAML里显式声明大文本inputs: long_text: type: string max_length: 50000然后在plugin.json的skills.input_schema里对应字段也要设maxLength: 50000。两边必须一致否则校验失败。6.5 坑五插件更新后skills不刷新现象改了skills CLI重新签名上传但Claude Code还在用旧版本。根因Claude Code有插件缓存缓存键是plugin.id plugin.version。只改代码不改version它认为是同一版本直接读缓存。解法每次更新skills逻辑必须升级plugin.json的version。我团队用npm version patch自动更新并在CI里加检查# CI脚本 if git diff HEAD~1 -- plugin.json | grep version; then echo Version updated, proceeding... else echo ERROR: plugin.json version not updated! exit 1 fi这些坑每一个都曾让我们加班到凌晨三点。现在我把它们列在这里不是为了炫耀经验而是告诉你Claude Code插件开发表面是写JSON和CLI实质是和一个严谨、安全、不讲情面的运行时系统打交道。尊重它的规则比追求技巧重要一百倍。