首页
/ RTK LLM Agent Hooks 深度解析:透明重写 CLI 命令,为 Claude Code、Cursor 等 10+ 编码 Agent 节省 60-90% Token

RTK LLM Agent Hooks 深度解析:透明重写 CLI 命令,为 Claude Code、Cursor 等 10+ 编码 Agent 节省 60-90% Token

2026-09-06 15:38:24作者:尤峻淳Whitney

RTK 的 hooks 目录承载了项目最核心的集成能力:部署到用户机器上的钩子工件(shell 脚本、TypeScript/Python 插件、Rust 二进制 hook),它们拦截 LLM 编码 Agent 发起的 CLI 命令并透明改写为 rtk 等价命令,使 90% 的 bash 输出被过滤后再进入 LLM 上下文——而 Agent 与用户的工作流完全无感。本文基于 hooks/README.md 完整展开这套钩子体系的架构设计、各 Agent 的 JSON 协议、命令重写注册表、退出码契约与优雅降级策略,并结合仓库源码给出实现级佐证。

1. 设计定位:Thin Delegates(薄委托层)

hooks/ 目录的定义非常克制:这里存放的是已部署的钩子工件(deployed hook artifacts)——由 rtk init 安装到用户机器上、运行在 Rust 二进制之外的文件。它们被明确要求为 thin delegates(薄委托层)

  • 解析各 Agent 特有的 JSON 输入;
  • 以子进程方式调用 rtk rewrite 做出重写决策;
  • 按 Agent 要求的 JSON 格式输出响应。

过滤逻辑一条都不写在钩子里(Zero filtering logic lives here)。这是 RTK hooks 体系的第一设计原则:所有 70+ 条重写规则的单一事实来源(single source of truth)是 Rust 二进制中的模式注册表 src/discover/registry.rs。这一点在部署脚本中被反复强调,例如 hooks/claude/rtk-rewrite.sh 的头部注释:

# This is a thin delegating hook: all rewrite logic lives in `rtk rewrite`,
# which is the single source of truth (src/discover/registry.rs).
# To add or change rewrite rules, edit the Rust registry — not this file.

职责边界同样清晰:hooks/ 拥有 10 个受支持 Agent 各自的钩子脚本与配置文件(Claude Code、Copilot、Cursor、Cline、Windsurf、Codex、OpenCode、Hermes、Pi、Mistral Vibe);不拥有钩子的安装/卸载(那是 src/hooks/init.rs)、重写模式注册表(src/discover/)或完整性校验(src/hooks/integrity.rs)。用一句话概括两者关系:src/hooks/ 负责“创建”这些文件,hooks/ 负责“存放”这些文件。仓库中实际还存在 antigravity/kilocode/ 两个子目录,各自包含 rules.md,属于同体系下以规则文件形式接入的新集成。

2. 工作原理:一条命令的改写全链路

文档给出的端到端流程如下:

Agent runs command (e.g., "cargo test --nocapture")
  -> Hook intercepts (PreToolUse / plugin event)
  -> Reads JSON input, extracts command string
  -> Calls `rtk rewrite "cargo test --nocapture"`
  -> Registry matches pattern, returns "rtk cargo test --nocapture"
  -> Hook sends response in agent-specific JSON format
  -> Agent executes "rtk cargo test --nocapture" instead
  -> Filtered output reaches LLM (up to 90% fewer bash output bytes)

这条链路中有两个关键支撑点,都可以在源码中验证:

(1)注册表匹配src/discover/registry.rs 在启动时用 RegexSet 一次性编译全部规则(见 registry.rs#L54-L62),并对命令做分类(Supported / Unsupported / Ignored);它还会剥离 sudoenvVAR=value 之类的前缀(源码中的 ENV_PREFIX 正则)再匹配,因此 FOO=bar git status 同样能被正确识别。

(2)rtk rewrite 退出码协议。这是整个 hook 体系的“通用语言”。src/hooks/rewrite_cmd.rs 的文档注释定义了精确契约:

Exit Stdout 含义
0 rewritten 重写被允许 —— hook 可自动放行改写后的命令
1 (空) 无 RTK 等价命令 —— hook 原样放行
2 (空) 命中 Deny 规则 —— 交给宿主 Agent 原生 deny 处理
3 rewritten 命中 Ask 规则 —— 重写但让宿主 Agent 向用户发起确认

每个 Agent 的钩子只需翻译这四种状态,无需理解重写规则本身。安全语义在测试中被显式锁定:rewrite_cmd.rs 中的 exit_code_protocol 测试模块特别断言 Default 判定必须映射到退出码 3(ask)而不是 0(allow)——否则任何没有显式权限规则的可重写命令都会被自动放行,绕过 Claude Code 的最小权限默认值。这是钩子体系中最重要的安全不变量。

3. 受支持 Agent 全景

文档给出的完整支持矩阵(机制、钩子类型、能否改写命令):

Agent Mechanism Hook Type Can Modify Command?
Claude Code Shell hook (PreToolUse) Transparent rewrite Yes (updatedInput)
VS Code Copilot Chat Rust binary (rtk hook copilot) Transparent rewrite Yes (updatedInput)
GitHub Copilot CLI Rust binary (rtk hook copilot) Deny-with-suggestion No(Agent 依据建议重试)
Cursor Rust binary Transparent rewrite Yes (updated_input)
Gemini CLI Rust binary (rtk hook gemini) Transparent rewrite Yes (hookSpecificOutput)
Cline / Roo Code Custom instructions (rules file) Prompt-level guidance N/A
Windsurf Custom instructions (rules file) Prompt-level guidance N/A
Codex CLI AGENTS.md / instructions Prompt-level guidance N/A
OpenCode TypeScript plugin (tool.execute.before) In-place mutation Yes
Pi TypeScript extension (tool_call event) In-place mutation Yes
Hermes Python plugin (pre_tool_call) In-place mutation Yes
Mistral Vibe Rust binary (rtk hook vibe) Transparent rewrite Yes (hook_specific_output.tool_input)

按实现形态可以归为三类,这正是“Integration Tiers”的分层依据(见第 8 节):

  1. Full hook(全钩子):shell 脚本或 Rust 二进制,通过 Agent 的 hook API 拦截命令,维护成本高——必须跟进 Agent API 变化;
  2. Plugin(插件):借助 Agent 自带的插件系统加载 TypeScript/JS/Python 代码,由 Agent 负责加载;
  3. Rules file(规则文件):纯 prompt 级指令(如 Cline 的 .clinerules、Windsurf 的 .windsurfrules、Codex 的 AGENTS.md 集成),没有可破坏的代码,维护成本最低。

每个 Agent 子目录都带有独立的 README,例如 hooks/claude/README.mdhooks/copilot/README.mdhooks/opencode/README.md,覆盖各自的安装细节。

4. 形态一:Shell 钩子(Claude Code / Cursor)

4.1 Claude Code 钩子的完整生命周期

部署文件是 hooks/claude/rtk-rewrite.sh,其运行流程为:

  1. 依赖检查jq 不存在时向 stderr 输出警告并 exit 0rtk 不在 PATH 时同样警告后 exit 0——命令将以原始形态执行。
  2. 版本守卫rtk rewrite 是 0.23.0 引入的命令。脚本解析 rtk --version 输出,若版本 < 0.23.0 则警告并以 0 退出;检查结果缓存在 $XDG_CACHE_HOME/rtk-hook-version-ok(默认 ~/.cache),避免每次 hook 调用都派生版本检查进程。
  3. 提取命令:从 stdin 读取 JSON,用 jq -r '.tool_input.command // empty' 提取命令;为空则直接退出。
  4. 委托决策REWRITTEN=$(rtk rewrite "$CMD" 2>/dev/null),然后用 case 分支翻译退出码(rtk-rewrite.sh#L58-L79):
case $EXIT_CODE in
  0)
    # 找到重写且无 deny/ask 规则 —— 可自动放行
    # 若输出与输入相同,说明命令本来就在用 RTK
    [ "$CMD" = "$REWRITTEN" ] && exit 0
    ;;
  1) exit 0 ;;   # 无 RTK 等价命令 —— 原样放行
  2) exit 0 ;;   # 命中 Deny —— 交给 Claude Code 原生 deny 处理
  3) ;;          # 命中 Ask —— 重写但保留用户确认
  *) exit 0 ;;
esac
  1. 格式化响应:退出码 3(ask)时输出不含 permissionDecision 字段的 JSON,强制 Claude Code 弹出用户确认;退出码 0(allow)时输出自动放行 JSON。

协议格式(stdin 输入 / stdout 输出):

{
  "tool_name": "Bash",
  "tool_input": { "command": "git status" }
}
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "permissionDecisionReason": "RTK auto-rewrite",
    "updatedInput": { "command": "rtk git status" }
  }
}

注意 allow 分支的一个细节:输出里的 updatedInput整个 tool_input 对象(脚本用 jq '.tool_input.command = $cmd | { … "updatedInput": .tool_input }' 构造),而非只替换 command 字符串——这保留了 Agent 传入的其他工具参数。

4.2 Cursor 钩子:JSON 契约更严格

hooks/cursor/rtk-rewrite.sh 输入格式与 Claude Code 相同,但 Cursor 要求所有路径都返回 JSON(文档明确:no rewrite 时必须返回 {})。它的差异点:

  • 无命令或无重写时输出 {}rtk-rewrite.sh#L36-L55);
  • 重写成功时输出 continue: true + permission + updated_input(下划线命名,区别于 Claude 的驼峰):
{
  "permission": "allow",
  "updated_input": { "command": "rtk git status" }
}
  • 退出码 3(ask)会被翻译为 permission: "ask"——注释说明这是为 Cursor 未来强制执行权限模型做的前置兼容(future-proof),当前 Cursor 尚未强制该字段。

5. 形态二:Rust 二进制钩子(Copilot / Gemini / Vibe)

这类 Agent 的钩子直接是 rtk 二进制自身的子命令处理器,实现位于 src/hooks/hook_cmd.rs。它们共享同一套“读 stdin → 解析 JSON → 决策 → 写 stdout”结构,但协议细节各不相同:

5.1 Copilot CLI:deny-with-suggestion 的降级策略

GitHub Copilot CLI 的输入是 camelCase 且 toolArgs被 JSON 字符串化的内层对象

{
  "toolName": "bash",
  "toolArgs": "{\"command\": \"git status\"}"
}

由于该 CLI 不支持 updatedInput(无法直接替换要执行的命令),hook 采用 deny-with-suggestion 策略——拒绝原始命令并附带建议,让 LLM 自己重试:

{
  "permissionDecision": "deny",
  "permissionDecisionReason": "Token savings: use `rtk git status` instead"
}

这是三种“改写能力”中唯一不能直接改命令、依赖 Agent 自我纠正的路径。

5.2 VS Code Copilot Chat

输入为 snake_case(与 Claude Code 相同的 tool_name/tool_input 结构),输出格式与 Claude Code 一致(含 updatedInput),属于完全透明改写。

5.3 Gemini CLI

输入中 tool_namerun_shell_command

{
  "tool_name": "run_shell_command",
  "tool_input": { "command": "git status" }
}

重写时输出 decision: allow + hookSpecificOutput.tool_input无重写时输出 {"decision": "allow"}(而不是空 stdout):

{
  "decision": "allow",
  "hookSpecificOutput": {
    "tool_input": { "command": "rtk git status" }
  }
}

源码 hook_cmd.rs#L436-L470 还揭示了两个文档未展开的实现细节:解析前先剥离 UTF-8 BOM(strip_leading_bom),且 tool_name 不是 run_shell_command 或命令为空时一律回退为 allow

5.4 Mistral Vibe

输入携带 hook_event_name: "pre_tool"session_id

{
  "tool_name": "bash",
  "tool_input": { "command": "git status" },
  "hook_event_name": "pre_tool",
  "session_id": "..."
}

重写时通过 hook_specific_output.tool_input 完成改写,并额外附带 system_message 让改写在 Vibe 的 UI 中可见:

{
  "hook_specific_output": {
    "tool_input": { "command": "rtk git status" }
  },
  "system_message": "rtk: rewrote to `rtk git status`"
}

Vibe 的“无意见”契约与众不同:no rewrite 时是 exit 0 + 空 stdout(而非输出任何 JSON)。src/hooks/hook_cmd.rs#L481-L487run_vibe() 的实现印证了这一点——run_vibe_inner 返回 None 时什么都不打印,且 JSON 解析失败仅写 stderr 后返回 None,保证进程仍以 0 退出。

6. 形态三:插件式 in-place mutation(OpenCode / Hermes / Pi)

这类集成运行在 Agent 的插件系统内部,拿到的是可变的工具参数对象,因此“输出 JSON”被替换为直接就地修改 command 字段。

6.1 OpenCode(TypeScript 插件)

hooks/opencode/rtk.ts 监听 tool.execute.before 事件,使用 Agent 提供的 zx 库调用 rtk rewrite

"tool.execute.before": async (input, output) => {
  const tool = String(input?.tool ?? "").toLowerCase()
  if (tool !== "bash" && tool !== "shell") return
  // ...
  const result = await $`rtk rewrite ${command}`.quiet().nothrow()
  const rewritten = String(result.stdout).trim()
  if (rewritten && rewritten !== command) {
    ;(args as Record<string, unknown>).command = rewritten
  }
}

两个值得注意的防御:插件加载时先用 which rtk 探测二进制,缺失则打警告并返回空插件(disabled);重写调用包裹在 try/catch 中,rtk rewrite 失败即静默放行。

6.2 Hermes(Python 插件)

hooks/hermes/rtk-rewrite/__init__.py 注册 pre_tool_call 钩子,只处理 tool_name == "terminal" 的调用:

ACCEPTED_REWRITE_RETURN_CODES = {0, 3}
EXPECTED_PASSTHROUGH_RETURN_CODES = {1, 2}

result = subprocess.run(
    ["rtk", "rewrite", command],
    shell=False, timeout=2, capture_output=True, text=True,
)
if result.returncode not in ACCEPTED_REWRITE_RETURN_CODES:
    # 1/2 是预期内的透传,静默返回;其他码打警告
    return
rewritten = result.stdout.strip()
if rewritten and rewritten != command:
    args["command"] = rewritten

实现上比文档代码片段更完整的地方包括:子进程设置了 2 秒超时TimeoutExpired 时警告放行);rtk 缺失时只警告一次(_rtk_missing_warned 标志);退出码 1/2 被显式区分——它们是预期内的透传码,不产生噪音日志。同目录的 tests/test_rtk_rewrite_plugin.py 提供了该插件的测试。

6.3 Pi

Pi 的集成是 TypeScript 扩展,监听 tool_call 事件,采用与 OpenCode 相同的“in-place mutation”模式,本地有一个 isBashToolCallEvent 守卫函数,安装位置在 ~/.pi/agent/extensions/,对应文件为 hooks/pi/rtk.ts

6.4 规则文件类集成(Cline / Windsurf / Codex)

这三个 Agent 不提供可编程 hook,RTK 以 prompt 级指令接入:Cline 写项目本地的 .clineruleshooks/cline/rules.md),Windsurf 写 workspace 级 .windsurfruleshooks/windsurf/rules.md),Codex 则在 $CODEX_HOME~/.codex/ 下放置 awareness 文档并集成进 AGENTS.mdhooks/codex/rtk-awareness.md)。这类集成的“可修改命令能力”是 N/A——它只是提示 LLM 主动使用 rtk 前缀,没有运行时拦截。

7. 命令重写注册表:规则、复合命令与覆盖开关

注册表(src/discover/registry.rs)覆盖的命令类别与预期节省幅度:

Category Examples Savings
Test Runners vitest, pytest, cargo test, go test, playwright 90-99%
Build Tools cargo build, npm, pnpm, dotnet, make 70-90%
VCS git status/log/diff/show 70-80%
Language Servers tsc, mypy 80-83%
Linters eslint, ruff, golangci-lint, biome 80-85%
Package Managers pip, cargo install, pnpm list 75-80%
File Operations ls, find, grep, cat, head, tail 60-75%
Infrastructure docker, kubectl, aws, terraform 75-85%

7.1 复合命令处理

注册表能正确处理 &&||;||&& 六种操作符,规则如下:

  • 管道(|:生产者与中间阶段保持原始命令,只有 pipeline-safe 的最终阶段被重写;
  • stderr 管道(|&:整条 pipeline 保持原始(因为过滤会破坏 stderr 合并语义);
  • And/Or/Semicolon(&&||;:两侧独立重写;
  • pipeline-safe 规则:初期仅限于参数安全的 greprgwc 调用;带 pattern-file 形式的搜索会被推迟处理。

典型示例:cargo fmt --all && cargo testrtk cargo fmt --all && rtk cargo test(两个阶段各自独立重写)。

7.2 覆盖控制(Override Controls)

三个正交的逃生阀:

  1. RTK_DISABLED=1:逐命令覆盖。RTK_DISABLED=1 git status 直接以原始命令运行;
  2. exclude_commands:配置在 ~/.config/rtk/config.toml,列出永不重写的命令。匹配基于剥离环境变量前缀后的完整命令,支持子命令模式("git push" 会排除 git push origin main);以 ^ 开头的条目按正则处理。源码侧可验证:src/core/config.rs#L31hooks.exclude_commands: Vec<String> 是该配置项的定义,rewrite_cmd.rs 在入口处加载它并传给 registry::rewrite_command()
  3. Already-RTK 直通rtk git status 原样通过,绝不会出现 rtk rtk git 的递归前缀(rewrite_cmd.rs 的测试 test_run_already_rtk_returns_some 显式固化了这一行为)。

8. 退出码契约与优雅降级

8.1 退出码契约(Exit Code Contract)

这是 hook 体系的第一铁律:钩子绝不能阻塞命令执行。所有错误路径(二进制缺失、JSON 解析失败、重写失败)都必须 exit 0,让 Agent 的命令以原始形态运行——一个非零退出的 hook 会直接阻止用户命令执行。

对应地,无重写时钩子必须不产生任何输出;唯一的例外是 Cursor,它要求所有路径都返回 JSON(no rewrite 时返回 {})。

文档同时诚实地标注了一个已知缺口(Gaps)hook_cmd.rs::run_gemini() 在收到非法 JSON 输入时会以退出码 1 结束而非退出 0。阅读源码可以确认这一点——hook_cmd.rs#L399-L404run_gemini_inner(&input).context(...)? 把 JSON 解析失败包装成 Err 返回,错误会沿 ? 传播到 main() 产生非零退出,违反非阻塞保证;而同文件的 run_vibe() 则是按契约实现的对照样例(解析失败仅写 stderr 并返回 None,进程正常退出)。这个标注为排查 Gemini 集成异常提供了明确依据。

8.2 优雅降级(Graceful Degradation)

文档列出的完整降级矩阵:

故障场景 行为
jq 未安装 警告到 stderr,exit 0(命令原始执行)
rtk 二进制不在 PATH 警告到 stderr,exit 0
rtk 版本过旧(< 0.23.0) 警告到 stderr,exit 0
JSON 输入非法 原样透传
rtk rewrite 崩溃 钩子 exit 0(子进程错误被忽略)
过滤逻辑错误 回退到原始命令输出

Claude Code 脚本的实现与这张表逐条对应:command -v jq / command -v rtk 检查、版本守卫分支、空 CMD 直接退出、case $EXIT_CODE in *) exit 0 的兜底分支,都是这层保证的具体落点。Hermes 插件则用超时(2s)+ 全量 try/except 覆盖了同等语义。

8.3 与安装/完整性子系统的关系

理解 hook 运行时需要知道它与 src/hooks/ 的生命周期层如何协作(见 src/hooks/README.md):rtk init 安装时会计算 hook 文件的 SHA-256 并写入 ~/.claude/hooks/.rtk-hook.sha256(只读 0o444);运行时 integrity::runtime_check() 重新计算并比对,被篡改的 hook 会被阻止执行;rtk verify 可随时输出 PASS/FAIL/WARN/SKIP 的校验状态。此外,所有文件操作采用原子写(tempfile + rename)且操作幂等,重复执行 rtk init 是安全的。这意味着用户无需担心部署后的 hook 文件被意外改动——完整性校验本身就是退出码契约之外的一道防线。

9. 为新 Agent 添加集成:层级、门槛与维护

文档对新集成给出了清晰的准入框架:

集成层级(Integration Tiers)

Tier Mechanism Maintenance Examples
Full hook Shell 脚本或 Rust 二进制,经 Agent hook API 拦截 High —— 必须跟踪 Agent API 变化 Claude Code, Cursor, Copilot, Gemini
Plugin Agent 插件系统中的 TS/JS/Python 插件 Medium —— Agent 负责加载 OpenCode, Hermes, Pi
Rules file Agent 读取的 prompt 级指令 Low —— 没有会坏的代码 Cline, Windsurf, Codex

准入门槛(Eligibility)——四个条件缺一不可:

  • Agent 拥有已文档化且稳定的 hook/plugin API(不接受实验性/alpha 接口);
  • Agent 处于活跃维护状态(近 3 个月有提交活动);
  • 集成遵循退出码契约(所有错误路径 exit 0);
  • hook 输出精确匹配 Agent 期望的 JSON 格式。

维护策略:Agent API 变化导致 hook 失效时应及时更新;若 Agent 停止维护或 hook 无法修复,该集成可以附带 release note 被弃用。新集成还需遵守项目的 Design Philosophy(原文档指向 ../CONTRIBUTING.md#design-philosophy)。

落地到新集成的工作路径可以从现有代码反推:Rust 系 hook 在 src/hooks/hook_cmd.rs 增加处理器函数、在 main.rs 增加对应的 HookCommands 变体;shell 钩子参照 hooks/claude/rtk-rewrite.sh / hooks/cursor/rtk-rewrite.sh 的“检查 → 委托 → 翻译”三段式;插件参照 OpenCode/Hermes 的最小实现。测试方面,仓库提供 hooks/claude/test-rtk-rewrite.sh 这类逐 hook 的验证脚本,以及 tests/ 下的集成测试(如 tests/guard_integration_test.rs)覆盖守卫路径。

10. 小结

RTK 的 hooks 体系是一套值得参考的“多 Agent 集成”工程范式,其核心取舍可以概括为三点:

  1. 逻辑收敛:70+ 条重写规则只存在于 Rust 注册表中,钩子只做协议翻译。改规则只改一处,12 个 Agent 集成同时受益;
  2. 契约先行rtk rewrite 的四态退出码(0/1/2/3)+ 钩子的非阻塞退出码契约,把“权限语义”与“重写决策”解耦,使每个 Agent 的适配层薄到不足百行;
  3. 失败即透明:任何环节(依赖缺失、版本过旧、解析失败、子进程崩溃)都退化为“原样执行原始命令”,token 节省是收益,命令可用性是底线。

对读者而言,这套体系可直接复用为实践模板:如果你的项目也要为多个 LLM Agent 提供命令级干预能力,先定义一个统一的“决策子进程 + 退出码协议”,再为每个 Agent 写一个只做 JSON 翻译的薄适配层,是比在每处重复实现过滤逻辑可维护得多的架构。

登录后查看全文
热门项目推荐
相关项目推荐