rtk 的 GitHub Copilot 集成:PreToolUse 命令重写 Hook 的实现与验证
本文以 hooks/copilot/README.md 为骨架,深入剖析 rtk(一个将常见开发命令输出压缩 60-90% token 的 CLI 代理)如何为 GitHub Copilot 生态(VS Code Copilot Chat + Copilot CLI)安装一个纯 Rust 二进制的 PreToolUse Hook。读完后,你将掌握 Copilot Hook 的双输入格式协议、updatedInput 透明重写与 deny-with-suggestion 两种响应策略的取舍、rtk init --copilot 的完整安装/卸载流程,以及如何用仓库自带的测试脚本对 Hook 的 allow/deny/rewrite 决策进行端到端验证。
1. 集成定位:为什么 Copilot Hook 不用 Shell 脚本
rtk 的 hooks/ 目录存放的是"部署到用户机器上的 Hook 工件"。多数 Agent 集成(如 OpenCode、Hermes)采用"薄委托"脚本:解析 Agent 专属 JSON 后调用 rtk rewrite 子进程做决策。但 Copilot 是例外,其 README 明确列出了两条特有设计约束:
- 使用
rtk hook copilot这个 Rust 二进制(而非 shell 脚本)——零jq依赖; - 自动识别两种输入格式:VS Code Copilot Chat(snake_case 的
tool_name/tool_input)与 Copilot CLI(camelCase 的toolName/toolArgs,其中 args 是 JSON 字符串); - VS Code 格式:返回
updatedInput实现透明重写; - Copilot CLI 格式:返回
permissionDecision: "deny"加替代命令建议(当时的 CLI API 不支持updatedInput)。
选择 Rust 直读 stdin/stdout 的原因在 src/hooks/hook_cmd.rs 的模块注释中解释得很直接:Hook 输出走严格的 JSON 协议,脚本里任何多余的 stdout/stderr 输出都会污染协议(文件头注释甚至点名了 Claude Code 的一个相关 bug:意外输出会静默禁用 Hook)。此外,Rust 二进制天然跨平台,rtk hook copilot 无需 jq 即可在 Windows 上原生工作。
2. 安装流程:rtk init --copilot 到底写了什么
Copilot 集成的安装入口是 src/main.rs 中的 rtk init --copilot(项目级)与 rtk init --global --copilot(用户级),对应 src/hooks/init.rs 里的 run_copilot / run_copilot_global。关键路径常量定义在 src/hooks/constants.rs:
GITHUB_DIR = ".github"、HOOKS_SUBDIR = "hooks"、COPILOT_HOOK_FILE = "rtk-rewrite.json"COPILOT_INSTRUCTIONS_FILE = "copilot-instructions.md"COPILOT_USER_DIR = ".copilot"(可用COPILOT_HOME环境变量覆盖)
2.1 Hook 配置文件
项目级安装会把下面这份 COPILOT_HOOK_JSON(init.rs 第 4846-4859 行)写入 .github/hooks/rtk-rewrite.json:
{
"version": 1,
"hooks": {
"PreToolUse": [
{
"type": "command",
"command": "rtk hook copilot",
"cwd": ".",
"timeout": 5
}
]
}
}
源码注释记录了一个重要的演进:该文件早期还声明过一条 camelCase 的 preToolUse 条目以覆盖 Copilot CLI 的原生 schema,但实测发现 Copilot CLI 会把两个键当作独立 Hook 顺序执行,导致每次工具调用白白多起一个进程,而 PascalCase schema 本身就够 Copilot CLI 消费——因此现在只注册单条 PascalCase PreToolUse。
2.2 指令文件(Prompt 层引导)
安装同时会向 .github/copilot-instructions.md upsert 一个带 <!-- rtk-instructions v2 --> 标记的指令块(init.rs 第 4861-4888 行),内容即 hooks/copilot/rtk-awareness.md 所描述的行为规范:该文件在会话启动时被 VS Code Copilot Chat 与 Copilot CLI 共同加载,指示 Agent 自动为命令加 rtk 前缀。核心规则与示例:
# Instead of: Use:
git status rtk git status
git log -10 rtk git log -10
cargo test rtk cargo test
docker ps rtk docker ps
kubectl get pods rtk kubectl get pods
因此 Copilot 集成实际是双层防线:指令文件做 Prompt 层引导,PreToolUse Hook 做执行层兜底(safety net)——即使 Agent 忘了加前缀,Hook 也会在命令执行前拦截并改写。
2.3 用户级安装与卸载
rtk init --global --copilot将同一份 Hook 配置与指令块写入~/.copilot/(目录由 copilot_user_dir() 解析,COPILOT_HOME环境变量优先),使本机所有 Copilot CLI 会话生效;rtk init --uninstall --copilot只移除 RTK 管理的工件:删除rtk-rewrite.json、并从copilot-instructions.md中精确摘除 rtk-instructions 标记块(用户自己的指令内容原样保留,见 uninstall_copilot_at())。
安装完成后按 rtk-awareness.md 的建议验证:
rtk --version # 应输出 rtk X.Y.Z
rtk gain # 应显示 token 节省仪表盘(而不是 command not found)
which rtk # 确认二进制路径
命名冲突警示(原文档保留):如果
rtk gain报命令错误,可能装到的是同名的 Rust Type Kit(reachingforthejack/rtk)而非本项目,用which rtk排查后重新安装即可。
3. 输入协议:detect_format 如何区分两种 Copilot 格式
Hook 入口 run_copilot() 的管线是:
- 限额读取 stdin(上限 1 MiB,
STDIN_CAP),超限即报错退出; - 剥离前导 BOM 并 trim——部分 Windows 宿主(如 Cursor)会往 Hook stdin 前置 UTF-8 BOM,serde_json 对此直接报错;
- JSON 解析失败时仅写 stderr 警告、静默放行(保证 Hook 永不阻塞命令);
- 交给
detect_format()分派到对应处理分支。
格式识别逻辑(detect_format())可归纳为一张表:
| 输入键 | 匹配工具名 | 解析路径 | 归类 |
|---|---|---|---|
tool_name(snake_case) |
runTerminalCommand、run_in_terminal、Bash、bash |
/tool_input/command |
VsCode(updatedInput 透明重写) |
toolName(camelCase) |
bash、powershell |
toolArgs(JSON 字符串)反序列化后取 command |
CopilotCli(保留给未升级的旧安装) |
toolName(camelCase) |
run_in_terminal |
同上 | CopilotIde(JetBrains/IntelliJ 插件,只认顶层 deny 决策) |
| 其余工具/格式 | — | — | PassThrough(完全静默) |
几个值得注意的实现细节:
run_in_terminal是 VS Code Copilot Chat 真实的终端工具名(经线上 payload 抓取确认)。若匹配不上,detect_format会落入 PassThrough,Hook 对 VS Code 永远不触发——这是该分支注释里特别强调的坑;toolArgs是 JSON 编码的字符串而非嵌套对象,CopilotCli变体会携带完整解析后的args对象,改写时只替换command字段,保留宿主附带元数据(description、initial_wait、mode等);- 命令为空、非 shell 工具(如
editFiles、view、edit)一律走 PassThrough,Hook 不产生任何输出。
// VS Code Copilot Chat 输入(snake_case)
{ "tool_name": "Bash", "tool_input": { "command": "git status" } }
// Copilot CLI 输入(camelCase,toolArgs 为 JSON 字符串)
{ "toolName": "bash", "toolArgs": "{\"command\": \"git status\"}" }
4. 输出协议:透明重写 vs deny-with-suggestion
识别格式后,run_copilot() 分派到三个 handler,最终都收敛到同一个决策函数 decide_hook_action(cmd, Host):先查权限规则(Deny 直接拒绝;含"不可证明"语法的命令 Defer 放行),再调用 get_rewritten() 执行真正的重写——它先跳过 heredoc 命令,再从 ~/.config/rtk/config.toml 读取 hooks.exclude_commands 与 transparent_prefixes 配置,交给 src/discover/registry.rs 的模式注册表匹配,返回 rtk <cmd> 或 None(原样)。
4.1 VS Code Copilot Chat:updatedInput 透明重写
vscode_response_from_decision() 输出的 JSON:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecisionReason": "RTK auto-rewrite",
"updatedInput": { "command": "rtk git status" }
}
}
Agent 随即静默执行被改写的命令——无拒绝、无重试。注释里记录了两个克制的边界条件:
permissionDecision: "allow"只在用户显式配置了 Allow 规则时断言(AllowRewrite分支);- Default 判定或 Ask 规则命中的重写(
AskRewrite)省略该字段,把权限判定留给宿主自身的原生流程。原因是曾出现过的回归:无条件断言"ask"会让 Copilot CLI 对每条被改写的命令弹出不带"记住"选项的阻塞对话框。
4.2 Copilot CLI:deny-with-suggestion(及源码中的后续演进)
关联文档描述的历史行为是:检测到 camelCase 格式时返回
{
"permissionDecision": "deny",
"permissionDecisionReason": "Token savings: use `rtk git status` instead"
}
Copilot 读到 reason 后自行改跑 rtk git status。需要说明的是,从 hook_cmd.rs 的 HookFormat 枚举注释看,当前源码已演进:新的 rtk init --copilot 不再注册 camelCase 条目(Copilot CLI 自身遵循 PascalCase schema),因此 CopilotCli 分支主要服务于升级前未重新 init 的旧安装,其响应也已改为返回 modifiedArgs(携带保留宿主元数据的完整改写参数)的透明重写;而"只认顶层 deny 决策"的 JetBrains/IntelliJ Copilot 插件(toolName: run_in_terminal)走的 CopilotIde 分支则仍严格采用 deny-with-suggestion 策略,输出形如:
{
"permissionDecision": "deny",
"permissionDecisionReason": "RTK token optimization: re-run this command as `rtk git status` instead."
}
也就是说,文档中"VS Code 透明重写 + CLI 拒绝带建议"的二分法依然是理解这套协议的最佳心智模型,只是当前代码把"拒绝带建议"精确地保留给了不支持 updatedInput/modifiedArgs 的 IDE 宿主。
4.3 自愈机制:清理历史遗留的 camelCase 注册
一个很工程化的细节:当请求落入 CopilotCli 分支时,run_copilot() 会先执行 heal_legacy_copilot_configs()——检查项目级 .github/hooks/rtk-rewrite.json 与全局 ~/.copilot/hooks/rtk-rewrite.json 中是否残留旧版 preToolUse camelCase 条目,若 PascalCase 条目仍在则原子性地移除旧条目(临时文件 + rename,写失败自动清理),并对每次自愈写一条 self_heal 审计记录。
4.4 可观测性:审计日志
设置 RTK_HOOK_AUDIT=1 后,所有 rewrite/deny/skip 决策会追加到 ~/.local/share/rtk/hook-audit.log(audit_log_inner()),格式为 时间戳 | 动作 | 原命令 | 改写后命令,字段内换行与 | 会被转义以防日志注入——排查"Hook 为什么没改写我的命令"时这是第一手依据。
5. 测试套件:用 rtk hook copilot 验证全部决策路径
README 的 Testing 一节给出的入口是 bash hooks/test-copilot-rtk-rewrite.sh;仓库中该测试脚本实际位于 hooks/copilot/test-rtk-rewrite.sh(脚本自身的 Usage 注释仍沿用旧路径名),直接运行该文件即可。脚本支持 RTK 环境变量覆盖被测二进制(RTK="${RTK:-rtk}"),用 jq 构造 mock 的 preToolUse 输入,共四个断言分区:
分区 1 — Copilot CLI 应 deny 的命令(test_deny:断言 permissionDecision == "deny" 且 reason 包含期望的 rtk 命令):
git status → 期望 reason 含 "rtk git status"
git log --oneline -10 → 期望 reason 含 "rtk git log"
git diff HEAD → 期望 reason 含 "rtk git diff"
cargo test / cargo build → 期望 "rtk cargo test" / "rtk cargo build"
cargo clippy --all-targets → 期望 "rtk cargo clippy"
grep -rn pattern src/ → 期望 "rtk grep"
gh pr list → 期望 "rtk gh"
分区 2 — VS Code Copilot Chat 应重写(test_vscode_rewrite:断言 hookSpecificOutput.permissionDecision == "allow" 且 updatedInput.command 含 rtk 命令):git status、cargo test、gh pr list。
分区 3 — 静默放行(test_allow:断言 Hook 输出为空,exit 0):
- 已是 rtk 命令(
rtk git status)——保证不会出现rtk rtk双重前缀; - heredoc 输入(
cat <<'EOF' ... EOF)——heredoc 不做自动重写(对应源码中has_heredoc检查); - 未知命令(
htop、echo hello world)——注册表未覆盖的命令绝不干预; - 非 bash 工具(
view、edit、editFiles)——工具类型闸门生效。
分区 4 — 输出格式契约:逐字段断言 Copilot CLI 输出是合法 JSON、permissionDecision == "deny"、reason 含反引号包裹的 rtk 命令;VS Code 输出是合法 JSON、hookSpecificOutput.permissionDecision == "allow"、updatedInput.command 以 rtk 开头。
脚本退出码即失败用例数(exit $FAIL),可直接挂进 CI。注意一个易混淆点:测试脚本依赖 jq 构造 payload,但被测试的 Hook 二进制本身零 jq 依赖——这正是 README 特意强调的特性。
Rust 侧另有单元级覆盖:hook_cmd.rs 的 tests 模块 对 detect_format 的四种归类逐一断言(Bash、runTerminalCommand、run_in_terminal、camelCase bash/powershell、JetBrains run_in_terminal),与上面的端到端脚本互补。
6. 与其他 Agent 集成的对照
rtk-awareness.md 给出的集成对照表(转换为仓库内路径):
| Tool | Mechanism | Hook 输出 | 文件 |
|---|---|---|---|
| Claude Code | PreToolUse hook + updatedInput |
透明重写 | hooks/claude/rtk-rewrite.sh |
| VS Code Copilot Chat | PreToolUse hook + updatedInput |
透明重写 | .github/hooks/rtk-rewrite.json(由 rtk init 生成) |
| GitHub Copilot CLI | PreToolUse deny-with-suggestion |
拒绝 + 重试 | 同上(单条 PascalCase 注册) |
| OpenCode | 插件 tool.execute.before |
透明重写 | hooks/opencode/rtk.ts |
| (任意 Agent) | 自定义指令 | Prompt 层引导 | .github/copilot-instructions.md |
Copilot 与 Cursor 同属"Rust 二进制 Hook"家族,但协议不同:Cursor 要求所有路径返回 JSON(无重写时回 {},见 run_cursor()),而 Copilot 的放行语义是空输出 + exit 0——这与 hooks/README.md 的退出码契约一致:Hook 在任何错误路径(二进制缺失、JSON 非法、重写失败)都必须 exit 0,绝不能阻塞用户命令。
7. 小结与适用前提
- 协议:
rtk hook copilot从 stdin 读取 preToolUse JSON(1 MiB 上限、BOM 免疫),按 snake_case/camelCase 自动分流;VS Code 走updatedInput透明重写,不支持改写的宿主走 deny-with-suggestion。 - 安装:
rtk init --copilot(项目级.github/)或rtk init --global --copilot(用户级~/.copilot/,COPILOT_HOME可覆盖);卸载只清理 RTK 管理的两个文件。 - 配置逃生舱:
RTK_DISABLED=1单次跳过、~/.config/rtk/config.toml的hooks.exclude_commands永久排除、RTK_HOOK_AUDIT=1审计日志。 - 验证:
bash hooks/copilot/test-rtk-rewrite.sh覆盖 deny/rewrite/pass-through/输出格式四类断言;安装后按 hooks/copilot/rtk-awareness.md 的三条命令验证二进制可用性,注意同名工具冲突。 - 适用前提:Hook 二进制需已安装且在 PATH 中(
rtk未安装时 Hook 静默放行,命令原样执行);camelCase 分支的行为差异取决于宿主版本,升级后建议重新执行一次rtk init --copilot,残留的旧注册也会由自愈逻辑自动清理。
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 StartedRust0629
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