Gemini CLI 扩展开发最佳实践:从结构组织、安全加固到发布排错的完整指南
本文围绕 Gemini CLI 官方的扩展最佳实践文档展开,系统讲解扩展的目录组织、gemini extensions link 本地联调、GEMINI.md 上下文编写,以及最小权限、输入校验、敏感配置存储三大安全实践,并覆盖语义化版本、发布渠道与产物清理等发布规范。读完本文,你既能按照最佳实践搭建一个可长期维护的 Gemini CLI 扩展工程,也能结合仓库源码理解 excludeTools 过滤、sensitive 密钥入 keychain 等机制在 扩展管理器 中的真实落地方式。
扩展开发的整体思路
官方指南将扩展的生命周期划分为四个阶段:开发(Development)→ 安全(Security)→ 发布(Release)→ 测试验证与排错(Test & Troubleshooting)。核心观点是:开发扩展是一个轻量、快速迭代的流程,最佳实践的价值在于让迭代循环更短、让上线风险更低。
扩展的"身份"由根目录下的 gemini-extension.json 清单文件定义。从源码 extension.ts 中的 ExtensionConfig 接口可以确认,清单支持的字段包括:
export interface ExtensionConfig {
name: string;
version: string;
mcpServers?: Record<string, MCPServerConfig>;
contextFileName?: string | string[];
excludeTools?: string[];
settings?: ExtensionSetting[];
themes?: CustomTheme[];
plan?: { directory?: string };
migratedTo?: string;
}
其中 name 和 version 是必填项——扩展管理器的 loadExtensionConfig 在加载清单时会显式校验:
if (!rawConfig.name || !rawConfig.version) {
throw new Error(
`Invalid configuration in ${configFilePath}: missing ${!rawConfig.name ? '"name"' : '"version"'}`,
);
}
此外,清单加载时会经过 recursivelyHydrateStrings 变量替换(支持 extensionPath、workspacePath、pathSeparator 等上下文变量),因此在清单中引用路径时可以利用这些变量做跨平台适配。这也解释了后文排错章节中"扩展未加载"问题为何常出在清单本身:清单必须是合法 JSON、位于根目录、且 name/version 完整。
开发:如何组织一个可扩展的扩展工程
推荐的目录结构
简单的扩展可能只有几个文件,但官方对复杂项目推荐如下组织结构:
my-extension/
├── package.json
├── tsconfig.json
├── gemini-extension.json
├── src/
│ ├── index.ts
│ └── tools/
└── dist/
三条配套建议:
- 使用 TypeScript:官方强烈建议用 TypeScript 开发扩展,以获得类型安全与更好的开发体验;
- 源码与构建产物分离:源码放在
src/,构建输出到dist/; - 打包依赖:如果依赖较多,用
esbuild之类的工具把依赖 bundle 进产物,以减少安装耗时、避免依赖冲突。
这一建议与扩展的安装加载机制直接相关:扩展安装后,CLI 是从 dist/ 中已构建的产物启动其 MCP 服务的(见 bundle-browser-mcp.mjs 这类官方打包脚本的做法),因此保持 src/ → dist/ 的清晰分界能让"重新构建即可生效"的联调流程成立。
用 link 命令做本地迭代
开发期间不必每次改动都卸载重装扩展。进入扩展目录执行:
cd my-extension
gemini extensions link .
link 会以本地路径作为扩展源注册,之后重新构建项目并重启 CLI 会话,代码改动即可在 CLI 中生效。
从源码看,link 既是命令行子命令(link.ts),也是交互模式下的斜杠命令(extensionsCommand.ts 与 ACP 模式下的 extensions.ts 中都实现了 /extensions link <source>)。仓库内还附带了一组可直接 link 学习的最小示例扩展,如 exclude-tools 示例、mcp-server 示例 和 policies 示例,README 中给出了对应的 gemini extensions link 用法,是照着最佳实践起步的现成参考。
需要注意的一个实现细节:扩展的 MCP 配置、hooks、skills、agents 都支持变量水合(recursivelyHydrateStrings),但扩展是在会话启动时加载的(参见下文排错章节),所以 link 之后仍需重启 CLI 会话才能被识别。
有效利用 GEMINI.md
扩展目录中的 GEMINI.md 会作为上下文文件(对应清单中的 contextFileName 机制)提供给模型。官方建议:
- 聚焦目标:说明扩展的高层用途,以及如何与它提供的工具交互;
- 保持精炼:不要把详尽文档整个塞进去,使用清晰直接的语言;
- 给出示例:附上模型应如何使用某个具体工具或命令的简短示例。
仓库本身的 GEMINI.md 与扩展加载逻辑一致:扩展管理器在 loadExtension 中会收集 contextFiles,并在 toOutputString 的输出中列出该扩展携带的上下文文件——/extensions list 能看到每个扩展贡献了哪些上下文,是验证 GEMINI.md 是否被正确加载的直观手段。
安全:最小权限、输入校验与敏感配置
官方给出的安全总原则是:最小权限 + 严格的输入校验。你的 MCP 服务运行在用户机器上,任何未校验的输入都可能演变成任意代码执行或未授权的文件系统访问。
最小权限:用 excludeTools 收窄工具面
只申请 MCP 服务功能所必需的权限,不要给模型过大的访问面(例如在受限工具足够时就不要开放完整 shell 权限)。如果扩展使用了 run_shell_command 这类强能力工具,可以在 gemini-extension.json 中显式排除危险命令:
{
"name": "my-safe-extension",
"excludeTools": ["run_shell_command(rm -rf *)"]
}
这能确保即使模型尝试执行危险命令,CLI 也会将其拦截。
源码层面的验证:excludeTools 从清单读取后经 extension-manager.ts 传递到 GeminiCLIExtension.excludeTools,最终汇入全局的工具排除集合。config.ts 中的 mergeExcludeTools 把设置文件与扩展声明的排除项去重合并后写入有效设置的 tools.exclude,供策略引擎统一裁决;config.test.ts 中有一组专门的用例(should merge excludeTools from settings and extensions、should handle overlapping excludeTools between extensions 等)验证了设置与多个扩展之间排除项的合并与去重行为。
另外两个值得了解的细节:
- 扩展安装/更新时,
excludeTools的变化会触发同意(consent)确认——consent.ts 会向用户展示"该扩展将排除以下核心工具",防止扩展悄悄改变权限面; - 仓库自带的 exclude-tools 示例扩展 使用了同样的写法(
"excludeTools": ["run_shell_command(rm -rf)"]),可link后实测效果。
输入校验:MCP 工具是本地攻击面
MCP 服务运行在用户机器上,每个工具入参都必须校验。官方给出的路径校验范例:
// Example: Validating paths
if (!path.resolve(inputPath).startsWith(path.resolve(allowedDir) + path.sep)) {
throw new Error('Access denied');
}
这个模式值得注意两点:先 path.resolve 归一化再比较(挡住 ../ 相对路径逃逸);拼接 path.sep 再做前缀比较(避免 /allowed 被误判为 /allowed-evil 的子路径)。对任何会落到文件系统、shell 或子进程的入参,都应套用等价的"归一化 + 白名单前缀/枚举"校验。
保护敏感配置:sensitive: true 让密钥进入 keychain
如果扩展需要 API Key 等机密,在清单的 settings 中声明并标记 sensitive: true:
"settings": [
{
"name": "API Key",
"envVar": "MY_API_KEY",
"sensitive": true
}
]
官方说明:标记后密钥会存入系统 keychain,并在 CLI 输出中脱敏展示。
extensionSettings.ts 的源码印证了完整链路:
- 声明结构:
ExtensionSetting含name、description、envVar与可选的sensitive;注释明确"未设置值的 setting 视为非敏感"; - 写入分流:
maybePromptForSettings中,敏感项走keychain.setSecret(envVar, value),非敏感项写入扩展的.env文件(keychain 存储名按Gemini CLI Extensions <name> <id> [workspaceDir]构造,区分 user/workspace 作用域); - 读取分流:
getScopedEnvContents从.env文件解析非敏感值,从 keychain 读取敏感值,合并后作为 MCP 服务的环境变量注入;workspace 作用域的非敏感设置存放在工作区下的.env文件; - 交互形态:
promptForSetting对敏感项使用type: 'password'的输入提示(不显回显),非敏感项用type: 'text'——这正是"CLI 输出中脱敏"的具体实现; - 变更追踪:
getSettingsChanges对比新旧 setting 列表,分别处理新增敏感项(重新提示输入)、移除敏感项(keychain.deleteSecret清理)、非敏感项的.env增删,保证升级扩展时旧密钥不会残留在明文文件中。
extensionSettings.test.ts 对应的测试用例覆盖了"敏感设置入 keychain 后从 keychain 删除"、"初始设置时值为空则不写入敏感项"、"敏感项使用 password 类型提示"等关键路径,可直接作为理解行为的依据。
发布:语义化版本、发布渠道与产物清理
语义化版本(SemVer)
按 SemVer 语义清晰传达变更内容:
- Major(主版本):破坏性变更(例如重命名工具、改变工具参数);
- Minor(次版本):新功能(例如新增工具或命令);
- Patch(修订版):缺陷修复与性能改进。
version 是清单必填字段(见上文 loadExtensionConfig 的校验),用户通过 /extensions list 和 extensions update 流程看到的版本比较依赖这一约定。
用 Git 分支管理发布渠道
利用分支让用户在"稳定"与"尝新"之间自选:
# 安装稳定版本(默认分支)
gemini extensions install github.com/user/repo
# 安装开发版本
gemini extensions install github.com/user/repo --ref dev
从安装元数据机制看(extension-manager.ts 的 toOutputString),每个扩展会记录 Source、类型、Ref、releaseTag 等信息,/extensions list 输出中即可看到扩展当前钉在哪个分支/标签上,便于团队区分"dev 渠道装了什么"。
保持发布产物干净
使用 GitHub Releases 发布时,压缩包应只包含运行必需的文件(如 dist/、gemini-extension.json、package.json),排除 node_modules/ 和 src/ 以最小化下载体积。这与开发阶段"bundle 依赖"的建议一脉相承:既然依赖已打进 dist/,node_modules/ 对运行就是冗余的。
测试与验证:上线前的两道关卡
- 手动验证:用
gemini extensions link把扩展接入真实 CLI 会话。确认工具出现在**调试控制台(F12)**中,且自定义命令能正确解析。/help可列出所有可用命令及其来源,是核对"扩展命令是否注册、是否与用户/项目命令冲突"的快捷手段。 - 自动化测试:如果扩展包含 MCP 服务,用 Vitest 或 Jest 为工具逻辑编写单元测试;通过 mock 传输层(transport) 即可对 MCP 工具做隔离测试,无需真实启动整个 CLI。
排错:三类高频问题的诊断路径
扩展不加载
扩展没有出现在 /extensions list 中时,按顺序检查:
- 检查清单:确认
gemini-extension.json位于扩展根目录且是合法 JSON。源码中loadExtensionConfig对"文件不存在""缺name/version"都会抛错并记录日志; - 核对名称:清单
name字段必须与扩展目录名完全一致(加载时经validateName校验,且扩展 ID 由 name+安装元数据派生,名称不一致会导致启用状态、设置存储等按错误键位匹配); - 重启 CLI:扩展在会话启动时加载。修改清单或
link新扩展后,必须重启 Gemini CLI 才会生效。
MCP 服务故障
工具行为不符合预期时:
- 看日志:查看 CLI 日志,确认 MCP 服务是否启动失败;
- 脱离 CLI 直接跑命令:把清单里
mcpServers的command和args拿到终端直接执行,确认服务在 CLI 之外能正常启动——这一步能区分"服务本身坏了"还是"被 CLI 环境(环境变量、工作目录)影响"; - 调试控制台:交互模式下按 F12 打开调试控制台,检查工具调用与响应的实际内容。
命令冲突
自定义命令"没反应"时:
- 确认优先级:用户级和项目级命令优先于扩展命令。用带前缀的名称(如
/extension.command)显式调用扩展版本来验证; - 查命令来源:运行
/help查看全部命令及其来源,定位实际命中的是哪一份定义。
这一优先级语义与 CLI 的命令解析实现一致:SlashCommandResolver.ts 与 SlashCommandConflictHandler.ts 负责在多个来源(内置、用户、项目、扩展、MCP)之间解析同名命令并处理冲突,/help 列出的"来源"信息正是来自这套解析逻辑。
小结:一套可直接套用的扩展工程清单
- 工程结构:TypeScript +
src/→dist/分离 +esbuild打包依赖; - 联调循环:
gemini extensions link .+ 重新构建 + 重启会话,用 F12 调试控制台和/extensions list验证加载; - 上下文:精炼的
GEMINI.md,讲清用途、交互方式与调用示例; - 安全三件套:
excludeTools收窄工具面、所有工具入参做归一化白名单校验、机密一律sensitive: true入 keychain; - 发布规范:SemVer + 分支渠道(
--ref)+ 干净的发布产物; - 排错三查:查清单(JSON 合法性/名称一致)、查 MCP 启动命令、查命令优先级(
/help)。
以上做法均有仓库源码或示例佐证,可深入参考:扩展清单类型定义、扩展管理器、排除工具合并逻辑、敏感设置实现、官方示例扩展集,以及 writing-extensions、releasing 等配套文档。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00