Gemini CLI 自定义命令实战指南:用 TOML 打造带参数注入与 Shell 执行的提示词快捷键
本文基于 gemini-cli 官方文档 custom-commands.md 展开,系统讲解 Gemini CLI 自定义命令(Custom commands)的完整机制:命令文件的存放位置与优先级、命名空间规则、TOML 文件格式,以及 {{args}} 参数注入、!{...} Shell 命令执行、@{...} 文件内容注入三种动态化手段。读完本文,你既能按步骤创建、热加载可复用的项目级/全局命令,也能从 FileCommandLoader 与 prompt-processors 源码层理解每条注入语法的实际执行链路和安全边界。
核心概念:把高频提示词变成可复用命令
自定义命令让你把最喜欢、最常用的提示词保存为个人快捷键。命令可以只服务于某一个项目,也可以全局可用并跨所有项目生效,从而简化工作流、保证团队内行为一致。
命令的最终形态是一个斜杠命令:输入 /命令名 后,CLI 会把你在 TOML 文件中定义的提示词发送给 Gemini 模型。整个机制由 packages/cli/src/services/FileCommandLoader.ts 中的 FileCommandLoader 类驱动,它负责递归扫描命令目录、解析并校验 TOML 文件,并将合法的定义适配为可执行的 SlashCommand 对象。
文件位置与优先级
Gemini CLI 从以下位置发现命令,并按固定顺序加载:
- 用户命令(全局):位于
~/.gemini/commands/,在你工作的任何项目中都可用; - 项目命令(本地):位于
<your-project-root>/.gemini/commands/,仅对当前项目生效,可以提交到版本控制与团队共享。
如果项目目录下的命令与用户目录下的命令同名,项目命令始终生效。这允许项目用本地版本覆盖全局命令。
这两个路径在源码中有明确定义:packages/core/src/config/storage.ts 的 Storage.getUserCommandsDir()(L97)与 getProjectCommandsDir()(L297)分别返回用户与项目命令目录;对应的单元测试位于 packages/core/src/config/storage.test.ts。
从源码结构看,优先级的实现方式是"加载顺序 + 后者胜出":FileCommandLoader.ts 中 getCommandDirectories()(L209-L248)按 用户目录 → 项目目录 → 扩展目录 的顺序返回命令目录,加载方法 loadCommands() 的注释也写明 user/project 命令(无 extensionName 的)采用 "last wins" 策略,而扩展命令在冲突时会被改名以避免覆盖。名称冲突的协调工作最终委托给 packages/cli/src/services/SlashCommandResolver.ts 和 CommandService。此外还有两个源码细节值得注意:
- 若当前工作目录就是用户主目录(
storage.isWorkspaceHomeDir()),会跳过项目命令目录,避免与用户命令目录重合产生虚假的冲突告警(见 FileCommandLoader.ts L221-L228); - 若开启了文件夹信任(folder trust)且当前目录不受信,
loadCommands()会直接返回空列表,即不受信目录下不加载任何自定义命令(L96-L98)。
命名与命名空间
命令名由文件路径相对于其 commands 目录的相对路径决定。子目录用于创建带命名空间的命令,路径分隔符(/ 或 \)会被转换为冒号(:):
~/.gemini/commands/test.toml中的文件 → 命令/test;<project>/.gemini/commands/git/commit.toml中的文件 → 命名空间命令/git:commit。
源码中这段转换逻辑在 parseAndAdaptFile()(FileCommandLoader.ts L302-L320)中,官方文档没有细说的规则有两点:
- 每个路径段会先做清洗:
segment.replace(/[^a-zA-Z0-9_\-.]/g, '_'),即字母、数字、_、-、.之外的字符一律替换为下划线。由于:是命名空间分隔符,这一步保证了文件名中的冒号不会引发命名冲突; - 单段清洗后超过 50 个字符会被截断为前 47 个字符加
...,防止 UI 溢出。
创建或修改 .toml 命令文件后,运行 /commands reload 可在不重启 CLI 的情况下重新加载;运行 /commands list 可查看全部可用命令文件。这两个内置子命令的实现位于 packages/cli/src/ui/commands/commandsCommand.ts:/commands list 调用 FileCommandLoader.listAvailableFiles() 按 User / Project / Extension 分组列出所有 .toml 文件;/commands reload 调用 context.ui.reloadCommands() 触发完整的命令再发现(包括用户/项目 TOML、MCP prompts 与扩展命令),reload 还有别名 refresh。
TOML 文件格式(v1)
命令定义文件必须使用 TOML 格式、以 .toml 为扩展名。加载器用 @iarna/toml 解析,并用 Zod 做校验,其 Schema 就是字段定义的唯一事实来源(FileCommandLoader.ts L55-L61):
const TomlCommandDefSchema = z.object({
prompt: z.string({
required_error: "The 'prompt' field is required.",
invalid_type_error: "The 'prompt' field must be a string.",
}),
description: z.string().optional(),
});
必填字段
prompt(String):命令执行时发送给 Gemini 模型的提示词,可以是单行或多行字符串。
可选字段
description(String):一行简短描述命令的作用,会显示在/help菜单中你的命令旁边。如果省略该字段,会基于文件名生成一段通用描述——源码中的兜底文案是`Custom command from ${path.basename(filePath)}`(FileCommandLoader.ts L323-L326),并且最终描述会被sanitizeForDisplay()截断到 100 字符以内;来自扩展的命令还会在描述前加上[扩展名]前缀。
校验不通过的文件会被跳过,同时通过 coreEvents.emitFeedback 输出一条 [FileCommandLoader] Skipping invalid command file 的错误反馈,方便排查(L289-L298)。解析失败的单测覆盖可参见 packages/cli/src/services/FileCommandLoader.test.ts。
参数处理:三种注入语法
自定义命令支持强大的参数处理机制。CLI 会根据 prompt 的内容自动选择正确的处理方式。在 packages/cli/src/services/prompt-processors/types.ts 中,三种语法的触发字符串被集中定义为常量:SHORTHAND_ARGS_PLACEHOLDER = '{{args}}'、SHELL_INJECTION_TRIGGER = '!{'、AT_FILE_INJECTION_TRIGGER = '@{'。
FileCommandLoader 在适配每个命令时,会按固定顺序组装一条"提示词处理流水线"(prompt processor pipeline,L332-L358):
- 若 prompt 含
@{...},先加AtFileProcessor(注释写明 "Security First":先注入文件内容,避免执行可能动态生成恶意@路径的 shell 命令); - 若含
!{...}或{{args}},加ShellProcessor; - 若不含
{{args}},最后加DefaultArgumentProcessor。
这个顺序与下文"文件注入先于 shell 执行、shell 执行先于参数追加"的处理次序一致。
1. {{args}} 的上下文感知注入
当 prompt 包含占位符 {{args}} 时,CLI 会把用户在命令名后输入的文本替换到该占位符处。替换行为取决于使用位置:
A. 原始注入(shell 命令之外)
用在提示词主体中时,参数按用户原样注入。
示例(git/fix.toml):
# Invoked via: /git:fix "Button is misaligned"
description = "Generates a fix for a given issue."
prompt = "Please provide a code fix for the issue described here: {{args}}."
模型最终收到:
Please provide a code fix for the issue described here: "Button is misaligned".
B. 在 shell 命令中使用参数(!{...} 块内)
在 shell 注入块(!{...})内使用 {{args}} 时,参数会被自动 shell 转义后再替换。这样可以把参数安全地传给 shell 命令,保证生成的命令语法正确、安全,防止命令注入漏洞。
示例(/grep-code.toml):
prompt = """
Please summarize the findings for the pattern `{{args}}`.
Search Results:
!{grep -r {{args}} .}
"""
执行 /grep-code It's complicated 时的流程:
- CLI 看到
{{args}}同时出现在!{...}之外和之内; - 外面:第一个
{{args}}被原始替换为It's complicated; - 里面:第二个
{{args}}被替换为转义后的版本(例如 Linux 下为"It\'s complicated"); - 实际执行的命令是
grep -r "It's complicated" .; - CLI 会弹出确认框,让你确认这条精确且安全的命令;
- 最终提示词发出。
这一行为在 packages/cli/src/services/prompt-processors/shellProcessor.ts 中逐段可见:
- 块外文本用
segment.replaceAll(SHORTHAND_ARGS_PLACEHOLDER, userArgsRaw)做原始替换(L155-L161、L208-L213); - 块内命令用
escapeShellArg(userArgsRaw, shell)生成转义参数(L97-L98),再command.replaceAll(..., userArgsEscaped)完成替换(L108-L111); - 若命令中没有 shell 触发符或没有闭合的注入块,则退化为纯
{{args}}原始替换(L71-L75、L90-L95)。相关行为有专门测试覆盖,见 shellProcessor.test.ts。
2. 默认参数处理(不含 {{args}} 时)
如果 prompt 没有 {{args}} 占位符,CLI 使用默认行为:
- 提供了参数(如
/mycommand arg1):CLI 把完整输入的命令追加到提示词末尾,中间以两个换行分隔,让模型同时看到原始指令和你刚提供的参数; - 未提供任何参数(如
/mycommand):提示词原样发出,不做任何追加。
对应实现是 packages/cli/src/services/prompt-processors/argumentProcessor.ts 中的 DefaultArgumentProcessor:当 context.invocation?.args 存在时,调用 appendToLastTextPart(prompt, context.invocation.raw) 把原始调用文本追加到最后一段文本上,并明确注释"该处理器仅在 prompt 不含 {{args}} 时使用"。
示例(changelog.toml):下面这个例子展示了如何定义模型角色、说明用户输入的位置,并规定期望的格式与行为,从而构建一个健壮命令。
# In: <project>/.gemini/commands/changelog.toml
# Invoked via: /changelog 1.2.0 added "Support for default argument parsing."
description = "Adds a new entry to the project's CHANGELOG.md file."
prompt = """
# Task: Update Changelog
You are an expert maintainer of this software project. A user has invoked a command to add a new entry to the changelog.
**The user's raw command is appended below your instructions.**
Your task is to parse the `<version>`, `<change_type>`, and `<message>` from their input and use the `write_file` tool to correctly update the `CHANGELOG.md` file.
## Expected Format
The command follows this format: `/changelog <version> <type> <message>`
- `<type>` must be one of: "added", "changed", "fixed", "removed".
## Behavior
1. Read the `CHANGELOG.md` file.
2. Find the section for the specified `<version>`.
3. Add the `<message>` under the correct `<type>` heading.
4. If the version or type section doesn't exist, create it.
5. Adhere strictly to the "Keep a Changelog" format.
"""
执行 /changelog 1.2.0 added "New feature" 时,发给模型的最终文本就是上面的原始 prompt,加上两个换行,再加上你输入的完整命令。
3. 用 !{...} 执行 shell 命令
你可以在 prompt 中直接执行 shell 命令并注入其输出,让命令变得动态化,非常适合从本地环境收集上下文——例如读取文件内容、检查 Git 状态。
当自定义命令尝试执行 shell 命令时,Gemini CLI 会先征求你的确认。这是安全措施,确保只有预期内的命令才能被执行。
工作原理:
- 注入命令:使用
!{...}语法; - 参数替换:块内如有
{{args}},自动 shell 转义(见上文第 1 节 B 部分); - 健壮解析:解析器能正确处理包含嵌套花括号的复杂 shell 命令,例如 JSON 载荷。
!{...}内的内容必须花括号配对({与})。要执行含未配对花括号的命令,建议把命令包进外部脚本文件,再在!{...}块内调用该脚本; - 安全检查与确认:CLI 对最终解析出的命令(参数转义替换之后)执行安全检查,弹窗展示将要执行的精确命令;
- 执行与错误报告:命令被执行。若命令失败,注入提示词的内容会包含错误信息(stderr)加一行状态,例如
[Shell command exited with code 1],帮助模型理解失败上下文。
与上述步骤一一对应的源码逻辑在 shellProcessor.ts:
- 嵌套花括号由 injectionParser.ts 中的
extractInjections()处理,它用简单的花括号计数匹配闭合位置,不支持转义;遇到未闭合的注入块会抛出Unclosed injection错误; - 安全检查通过策略引擎完成:
config.getPolicyEngine().check({ name: 'run_shell_command', args: { command } })(L127-L133)。结果为DENY时直接抛错Blocked command ... Blocked by policy;结果为ASK_USER时把命令收集起来,最终抛出ConfirmationRequiredError交由 UI 层弹确认框(L135-L150); - 已被本次会话 allowlist 放行过的命令会跳过确认(L122-L124);
- 执行走
ShellExecutionService.execute(),失败时按情况追加[Shell command '...' exited with code N]、[... aborted]或[... terminated by signal ...]状态行(L180-L202)。
示例(git/commit.toml):该命令取暂存的 git diff,让模型据此写提交信息。
# In: <project>/.gemini/commands/git/commit.toml
# Invoked via: /git:commit
description = "Generates a Git commit message based on staged changes."
# The prompt uses !{...} to execute the command and inject its output.
prompt = """
Please generate a Conventional Commit message based on the following git diff:
```diff
!{git diff --staged}
```
"""
执行 /git:commit 时,CLI 先运行 git diff --staged,再在把最终完整提示词发给模型之前,用该命令的输出替换 !{git diff --staged}。
4. 用 @{...} 注入文件内容
可以使用 @{...} 语法把文件或目录列表的内容直接嵌入提示词,非常适合编写针对特定文件操作的命令。
工作原理:
- 文件注入:
@{path/to/file.txt}被替换为file.txt的内容; - 多模态支持:路径指向受支持的图片(如 PNG、JPEG)、PDF、音频或视频文件时,会正确编码并以多模态输入注入。其他二进制文件会被妥善处理并跳过;
- 目录列表:
@{path/to/dir}会遍历目录,把该目录及所有子目录中的文件插入提示词,并遵循启用时的.gitignore与.geminiignore规则; - 工作区感知:命令会在当前目录及其他工作区目录中查找路径。工作区内的绝对路径是允许的;
- 处理顺序:
@{...}的文件注入在 shell 命令(!{...})和参数替换({{args}})之前处理; - 解析:解析器要求
@{...}内的路径花括号配对({与})。
实现位于 packages/cli/src/services/prompt-processors/atFileProcessor.ts:每个注入块通过 readPathFromWorkspace() 从工作区解析路径并返回多模态 parts;若文件被 .gitignore/.geminiignore 忽略而未包含,UI 会提示一条 info 消息;若注入失败,会在 UI 中报 error,并保留原始占位符在提示词中(而不是静默丢弃),该行为有 atFileProcessor.test.ts 的测试佐证。
示例(review.toml):该命令注入一份固定的最佳实践文件(docs/best-practices.md),并用用户参数提供评审上下文。
# In: <project>/.gemini/commands/review.toml
# Invoked via: /review FileCommandLoader.ts
description = "Reviews the provided context using a best practice guide."
prompt = """
You are an expert code reviewer.
Your task is to review {{args}}.
Use the following best practices when providing your review:
@{docs/best-practices.md}
"""
执行 /review FileCommandLoader.ts 时,@{docs/best-practices.md} 占位符先被该文件内容替换,{{args}} 再被你的输入替换,最终提示词才发给模型。
完整示例:一个"纯函数"重构命令
下面创建一个全局命令,让模型把代码重构成纯函数。
1. 创建目录和文件
先确保用户命令目录存在,再创建 refactor 子目录用于组织,最后创建 TOML 文件。
macOS / Linux:
mkdir -p ~/.gemini/commands/refactor
touch ~/.gemini/commands/refactor/pure.toml
Windows(PowerShell):
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.gemini\commands\refactor"
New-Item -ItemType File -Force -Path "$env:USERPROFILE\.gemini\commands\refactor\pure.toml"
2. 写入文件内容
用编辑器打开 ~/.gemini/commands/refactor/pure.toml,加入以下内容。最佳实践是包含可选的 description 字段。
# In: ~/.gemini/commands/refactor/pure.toml
# This command will be invoked via: /refactor:pure
description = "Asks the model to refactor the current context into a pure function."
prompt = """
Please analyze the code I've provided in the current context.
Refactor it into a pure function.
Your response should include:
1. The refactored, pure function code block.
2. A brief explanation of the key changes you made and why they contribute to purity.
"""
3. 运行命令
完成。现在可以在 CLI 中运行该命令了。先往上下文里添加一个文件,再调用命令:
> @my-messy-function.js
> /refactor:pure
Gemini CLI 随后就会执行你在 TOML 文件中定义的多行提示词。如果创建或修改文件后命令没有出现,输入 /commands list 确认文件已被扫描到,再执行 /commands reload 热加载。
小结
| 主题 | 关键点 | 源码位置 |
|---|---|---|
| 命令发现 | 用户 ~/.gemini/commands/ → 项目 .gemini/commands/ → 扩展,glob 递归扫描 **/*.toml |
FileCommandLoader.ts |
| 优先级 | 同名时项目命令覆盖用户命令(last wins,由 resolver 协调) | SlashCommandResolver.ts、CommandService.ts |
| 字段校验 | Zod Schema:prompt 必填字符串、description 可选 |
FileCommandLoader.ts L55-L61 |
| 命名清洗 | 非法字符替换为 _,单段超 50 字符截断,分隔符转 : |
FileCommandLoader.ts L307-L320 |
{{args}} |
块外原始注入、!{...} 内 shell 转义注入 |
shellProcessor.ts |
| 默认参数 | 无 {{args}} 时把完整原始调用追加到提示词末尾 |
argumentProcessor.ts |
!{...} |
嵌套花括号解析、策略引擎检查、用户确认、退出码状态行 | injectionParser.ts、shellProcessor.ts |
@{...} |
文件/目录/多模态注入,工作区感知,先于 shell 与参数处理 | atFileProcessor.ts |
| 管理命令 | /commands list、/commands reload(别名 refresh) |
commandsCommand.ts |
掌握以上内容后,你可以把团队的高频工作流(写 changelog、生成提交信息、按规范评审代码等)沉淀为可版本控制、可热加载、且参数经过安全转义的斜杠命令,显著降低重复提示词的成本并统一团队的使用方式。
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 StartedRust0623
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