首页
/ rtk 的 GitHub Copilot 集成:PreToolUse 命令重写 Hook 的实现与验证

rtk 的 GitHub Copilot 集成:PreToolUse 命令重写 Hook 的实现与验证

2026-09-06 13:03:36作者:仰钰奇

本文以 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_JSONinit.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() 的管线是:

  1. 限额读取 stdin(上限 1 MiB,STDIN_CAP),超限即报错退出;
  2. 剥离前导 BOM 并 trim——部分 Windows 宿主(如 Cursor)会往 Hook stdin 前置 UTF-8 BOM,serde_json 对此直接报错;
  3. JSON 解析失败时仅写 stderr 警告、静默放行(保证 Hook 永不阻塞命令);
  4. 交给 detect_format() 分派到对应处理分支。

格式识别逻辑(detect_format())可归纳为一张表:

输入键 匹配工具名 解析路径 归类
tool_name(snake_case) runTerminalCommandrun_in_terminalBashbash /tool_input/command VsCodeupdatedInput 透明重写)
toolName(camelCase) bashpowershell 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 字段,保留宿主附带元数据(descriptioninitial_waitmode 等);
  • 命令为空、非 shell 工具(如 editFilesviewedit)一律走 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_commandstransparent_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.rsHookFormat 枚举注释看,当前源码已演进:新的 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.logaudit_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 statuscargo testgh pr list

分区 3 — 静默放行test_allow:断言 Hook 输出为空,exit 0):

  • 已是 rtk 命令(rtk git status)——保证不会出现 rtk rtk 双重前缀;
  • heredoc 输入(cat <<'EOF' ... EOF)——heredoc 不做自动重写(对应源码中 has_heredoc 检查);
  • 未知命令(htopecho hello world)——注册表未覆盖的命令绝不干预;
  • 非 bash 工具(viewediteditFiles)——工具类型闸门生效。

分区 4 — 输出格式契约:逐字段断言 Copilot CLI 输出是合法 JSON、permissionDecision == "deny"、reason 含反引号包裹的 rtk 命令;VS Code 输出是合法 JSON、hookSpecificOutput.permissionDecision == "allow"updatedInput.commandrtk 开头。

脚本退出码即失败用例数(exit $FAIL),可直接挂进 CI。注意一个易混淆点:测试脚本依赖 jq 构造 payload,但被测试的 Hook 二进制本身零 jq 依赖——这正是 README 特意强调的特性。

Rust 侧另有单元级覆盖:hook_cmd.rs 的 tests 模块detect_format 的四种归类逐一断言(BashrunTerminalCommandrun_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.tomlhooks.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,残留的旧注册也会由自愈逻辑自动清理。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
916
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388