rtk 的 Cursor 集成实战:preToolUse 钩子如何透明改写 Agent 命令以削减 Token 消耗
2026/9/7 1:18:57 网站建设 项目流程

rtk 的 Cursor 集成实战:preToolUse 钩子如何透明改写 Agent 命令以削减 Token 消耗

【免费下载链接】rtkCLI proxy that reduces LLM token consumption by 60-90% on common dev commands. Single Rust binary, zero dependencies项目地址: https://gitcode.com/GitHub_Trending/rtk4/rtk

本文基于 rtk(Rust Token Killer)仓库中 Cursor 钩子文档 及其配套实现展开,讲解 rtk 如何借助 Cursor 的preToolUse钩子在 Agent 执行 Shell 命令前将其透明改写为rtk等价命令(例如git statusrtk git status),从而让 LLM 看到最多削减 90% 的 bash 输出字节。读完本文,你将掌握 Cursor 钩子的 JSON 协议格式(permission/updated_input)、原生二进制钩子rtk hook cursor的完整处理链、遗留 shell 脚本rtk-rewrite.sh的委派机制,以及通过rtk init --global --agent cursor安装/卸载钩子的全部细节。

1. Cursor 集成的整体架构

在 rtk 的 Agent 钩子总览 中,所有 Agent 集成都被明确划分为三层,各司其职:

位置职责
部署产物(Deployed artifacts)hooks/cursor/安装在用户机器上的钩子文件本体
安装/卸载器src/hooks/init.rsrtk init/rtk uninstall的写入、补丁、备份与原子写逻辑
原生运行时src/hooks/hook_cmd.rsrtk hook cursor子命令的 JSON 协议处理与响应构造

整体数据流(来自 hooks/README.md):

Agent 执行命令(例如 "cargo test --nocapture") -> 钩子拦截(Cursor preToolUse) -> 读取 stdin 上的 JSON,提取命令字符串 -> 调用改写注册表(src/discover/registry.rs) -> 返回 "rtk cargo test --nocapture" -> 钩子以 Cursor 专属 JSON 格式输出响应 -> Agent 实际执行改写后的命令 -> 经过滤的输出进入 LLM 上下文(bash 输出字节最多减少 90%)

关键点:所有改写规则(70+ 条模式)集中在 Rust 侧的注册表 src/discover/registry.rs,钩子脚本只做“解析 Agent 专属 JSON + 调用改写逻辑 + 格式化响应”三件事,自身不包含任何过滤逻辑。

Cursor 集成有两个历史形态,本文都会覆盖:

  1. 原生二进制钩子(当前默认)rtk init --global --agent cursor~/.cursor/hooks.json中注册rtk hook cursor命令,由 Rust 二进制直接处理 stdin/stdout;
  2. 遗留 shell 脚本:hooks/cursor/rtk-rewrite.sh,仓库中保留的薄委派脚本,依赖jqrtk >= 0.23.0,作为旧版安装的兼容形态保留。

2. Cursor 钩子的 JSON 协议

Cursor 钩子说明 强调了两个与 Claude Code 钩子的核心差异,这是理解整个集成协议的基础:

2.1 响应字段命名不同

Cursor 使用permission/updated_input,而 Claude Code 使用hookSpecificOutput/updatedInput。输入格式两者相同:

输入(stdin,与 Claude Code 一致):

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

输出(stdout,发生改写时):

{ "permission": "allow", "updated_input": { "command": "rtk git status" } }

2.2 所有路径都必须输出 JSON

这是 Cursor 独有的契约:当没有任何改写适用时,钩子必须返回{}(空 JSON 对象),而不是像其他 Agent 钩子那样输出空字符串。Cursor 要求钩子在任何执行路径上都要给出合法的 JSON 输出,否则协议会解析失败。

这一契约在原生实现中被严格兑现。src/hooks/hook_cmd.rs 中的run_cursor()覆盖了所有边界情况:

  • 空 stdin:输出{}
  • JSON 解析失败:输出{}
  • 解析成功但tool_input.command缺失或为空:输出{}
  • 权限判定为Deny(命中 deny 规则)或Defer(无改写/不可证明构造):同样输出{},把原命令交还给 Cursor 原生流程处理。

只有在判定为AllowRewriteAskRewrite时才输出带updated_input的响应,分别对应两种权限值:

// AllowRewrite(命中 Cursor 的 allow 规则) { "continue": true, "permission": "allow", "updated_input": { "command": "rtk git status" } } // AskRewrite(默认判定,未配置 allow 规则) { "continue": true, "permission": "ask", "updated_input": { "command": "rtk git status" } }

这两个响应的构造分别由cursor_allow()cursor_ask()完成(hook_cmd.rs)。permission: "ask"意味着把改写后的命令交给 Cursor 的用户确认流程——这保证了 rtk 的自动放行集合永远是宿主权限模型的子集,不会比 Cursor 自身更宽松。

3. 原生钩子 run_cursor 的完整处理链

rtk hook cursor子命令在 src/main.rs 中分发到hooks::hook_cmd::run_cursor()。其处理链每一步都有明确的防御设计:

3.1 stdin 读取:1 MiB 上限

const STDIN_CAP: usize = 1_048_576; // 1 MiB

read_stdin_limited()限制单次读取上限为 1 MiB,超限即报错返回,防止恶意或异常的超大 payload 撑爆进程内存(hook_cmd.rs)。

3.2 BOM 剥离:针对 Windows 宿主的实测修复

let input = strip_leading_bom(&input).trim();

源码注释明确指出:部分 Windows 宿主会给钩子 stdin 前置 UTF-8 BOM(已在 Cursor 上确认),而serde_json会拒绝带 BOM 的输入。测试用例甚至覆盖了“双重 BOM”场景(tracer 包装rtk hook cursor时出现,Cursor 3.2.x 实测),见 hook_cmd.rs 测试。

3.3 命令提取与判定

命令通过 JSON 指针/tool_input/command提取,随后进入decide_hook_action(&cmd, permissions::Host::Cursor)(hook_cmd.rs)。判定逻辑分四档:

判定触发条件响应
AllowRewrite改写成功 + 命中 Cursor 的Shell(...)allow 规则permission: "allow"
AskRewrite改写成功 + 默认判定(无规则)permission: "ask"
Deny命中 Cursor deny 规则{}(不阻断,交还宿主)
Defer无可改写项 / 含不可证明构造{}(原样透传)

3.4 不可证明构造(unattestable constructs)一律透传

decide_from_verdict()(hook_cmd.rs)在改写前调用contains_unattestable_construct(cmd)做安全检查。对包含反引号替换、$(...)命令替换、文件重定向等构造的命令,即使权限判定是 Allow 也直接 Defer——因为 rtk 无法在改写前证明这些动态展开部分的最终行为。对应测试:

  • test_cursor_substitution_defers_even_when_allowedgit status \rm -rf /tmp/x`git status $(rm -rf /tmp/x)都返回{}`;
  • src/hooks/rewrite_cmd.rs 测试 同样验证了反引号、$()、双引号内替换、文件重定向(git log > /tmp/out.txt)均透传,而 fd 复制重定向(git status 2>&1)仍可改写。

3.5 改写注册表与覆盖控制

实际改写由 src/discover/registry.rs 的rewrite_command()完成,注册表按类别覆盖测试框架(vitest/pytest/cargo test 等,90-99% 节省)、构建工具(70-90%)、VCS(git status/log/diff,70-80%)、语言服务(tsc/mypy,80-83%)、Lint 器(80-85%)、包管理器(75-80%)与文件操作(60-75%)等模式(类别与节省比例见 hooks/README.md 的注册表表格)。

三类覆盖机制对 Cursor 钩子同样生效:

  1. RTK_DISABLED=1:环境变量前缀按命令禁用改写,例如RTK_DISABLED=1 git status原样执行。注册表在 env 前缀中检测到RTK_DISABLED=时跳过改写(registry.rs);
  2. exclude_commands:在~/.config/rtk/config.toml中列出永不改写的命令,支持子命令模式("git push"同时排除git push origin main)和^开头的正则模式;
  3. Already-RTK 幂等rtk git status保持原样,绝不会产生rtk rtk git。测试test_cursor_already_rtk_passthrough验证此时输出{}(hook_cmd.rs)。

复合命令方面,注册表处理&&||;||&&:管道中仅改写 pipeline-safe 的末段,&&/||/;两侧独立改写——例如cargo fmt --all && cargo test变为rtk cargo fmt --all && rtk cargo test(测试test_cursor_compound_rewrite_includes_continue验证了复合改写响应同样携带continue: true)。

3.6 权限规则来源:~/.cursor/cli-config.json

permissions::Host::Cursor的规则加载逻辑在 src/hooks/permissions.rs 的load_cursor_rules():读取全局~/.cursor/cli-config.json中的permissions.denypermissions.allow数组,只识别Shell(前缀的规则并剥离包装(裸Shell规则映射为*)。源码注释解释了设计约束:只读全局配置,因为 RTK 的自动放行集合必须是宿主信任集合的子集(宿主还会叠加项目级配置与 folder-trust),RTK 绝不能比 Cursor 更宽松。

测试矩阵(hook_cmd.rs 测试区)覆盖了该集成的全部关键路径:

测试验证点
test_cursor_rewrite_flat_formatallow 规则下git status输出扁平格式(无hookSpecificOutput包裹)
test_cursor_default_verdict_rewrites无规则时默认判定走ask而非allow
test_cursor_unallowed_segment_asksgit status && rm -rf /tmp/x中未授权段导致ask
test_cursor_passthrough_empty_json未支持命令(htop)返回{}
test_cursor_empty_input_empty_json空输入返回{}
test_cursor_heredoc_passthroughheredoc 命令透传返回{}
test_cursor_deny_blocks_rewritedeny 规则阻断改写

3.7 审计日志

设置RTK_HOOK_AUDIT=1后,每次 rewrite/ask/deny 都会追加写入~/.local/share/rtk/hook-audit.log,格式为时间戳 | 动作 | 原命令 | 改写后命令,字段做了反斜杠/竖线/换行转义以防日志注入(hook_cmd.rs)。这是排查“某条命令为什么被改写/没被改写”的第一手依据。

4. 遗留 shell 脚本 rtk-rewrite.sh:薄委派模式

hooks/cursor/rtk-rewrite.sh(67 行)是仓库保留的旧版安装产物,脚本头注释声明兼容 Cursor 编辑器与 cursor-cli(两者共享~/.cursor/hooks.json)。它体现了 rtk 钩子设计的“薄委派”原则——自身零过滤逻辑,全部改写决策委托给rtk rewrite子进程

INPUT=$(cat) CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty') if [ -z "$CMD" ]; then echo '{}' exit 0 fi # 委托给 Rust 二进制。 # 退出码:0 = 允许改写, 1 = 不改写(透传), 2 = 拒绝, 3 = 询问 REWRITTEN=$(rtk rewrite "$CMD" 2>/dev/null) RC=$?

4.1 前置守卫(Guards)

脚本开头有三层守卫,任一层失败都只向 stderr 打警告并exit 0

  1. jq缺失:警告并退出(jq是提取命令字段的唯一依赖);
  2. rtk不在 PATH:警告并退出;
  3. 版本守卫rtk rewrite子命令自 0.23.0 引入,脚本解析rtk --version输出,若MAJOR=0 && MINOR<23则警告并退出。

4.2 rtk rewrite 的退出码契约

rtk rewrite的退出码协议在 src/hooks/rewrite_cmd.rs 有权威定义:

退出码stdout含义
0改写后命令允许改写,钩子可自动放行
1无 RTK 等价项,钩子原样透传
2命中 deny 规则,交还宿主原生拒绝流程
3改写后命令命中 ask 规则,改写但由宿主发起用户确认

shell 脚本对该契约的解读是:只有 0 或 3 才继续构造响应;RC 为 3 时把permission置为ask(脚本注释说明 Cursor 当前未强制执行 ask 语义,此处属于“面向未来的预留”)。

4.3 优雅降级:永不阻断命令执行

hooks/README.md 的“Exit Code Contract”规定了所有钩子的一条铁律:任何错误路径(缺二进制、坏 JSON、改写失败)都必须退出 0,否则 Agent 的命令会被阻断。脚本中“改写结果与原命令相同则输出{}”的分支(第 52-55 行)也保证了幂等场景零副作用。

需要注意的是:当前新安装走的是原生二进制路径(见下一节),该脚本主要作为遗留安装的存在保留,rtk init --global --agent cursor会主动清理它。

5. 安装与卸载:rtk init / rtk uninstall

5.1 安装命令

rtk init --global --agent cursor # 等价简写:rtk init -g --agent cursor

Cursor 钩子是global-only:不带--globalrtk init会直接报错Cursor hooks are global-only. Use: rtk init -g --agent cursor(init.rs)。

5.2 安装流程(install_cursor_hooks)

install_cursor_hooks()(init.rs)执行四步:

第一步:迁移清理。若存在旧版脚本~/.cursor/hooks/rtk-rewrite.sh,删除它,并顺带清掉hooks.json中指向该脚本的陈旧条目(remove_legacy_cursor_hooks_json_entries)。

第二步:幂等检查cursor_hook_already_present()检查hooks.preToolUse数组中是否已有 command 包含rtk-rewrite.sh或等于rtk hook cursor的条目,已存在则跳过。

第三步:写入条目insert_cursor_hook_entry()(init.rs)向~/.cursor/hooks.json追加:

{ "version": 1, "hooks": { "preToolUse": [ { "command": "rtk hook cursor", "matcher": "Shell" } ] } }

其中"matcher": "Shell"把钩子触发范围限定在 Shell 工具上;命令常量CURSOR_HOOK_COMMAND = "rtk hook cursor"定义在 src/hooks/constants.rs。

第四步:备份 + 原子写。写前把原文件复制为hooks.json.bak,写入采用“同目录临时文件 + rename”的原子模式(atomic_write),崩溃也不会留下半写文件。完成后打印提示:Cursor reloads hooks.json automatically——Cursor 会自动重新加载hooks.json,无需重启。

安装路径常量:CURSOR_DIR = ".cursor"HOOKS_JSON = "hooks.json"(constants.rs)。

5.3 卸载

rtk uninstall --global --agent cursor

remove_cursor_hooks()(init.rs)做三件事:删除遗留脚本文件;从hooks.json中精确移除 RTK 条目(同时匹配遗留脚本路径与rtk hook cursor命令,其他用户自定义钩子不受影响);修改前同样先备份为hooks.json.bak再原子写。支持--dry-run预览全部动作而不落盘。

5.4 验证

安装后可以直接查看~/.cursor/hooks.json确认条目存在;日常调试建议开启审计日志观察真实改写行为:

export RTK_HOOK_AUDIT=1 # 在 Cursor Agent 中让 Agent 执行 git status 等命令 cat ~/.local/share/rtk/hook-audit.log

日志中会出现形如2026-09-06T13:00:00 | rewrite | git status | rtk git status的记录,或ask/deny/skip:defer等动作标记。

6. 适用前提与注意事项

综合 Cursor 钩子说明 与源码,使用该集成时需要注意以下边界:

  1. 版本要求:原生二进制钩子需要rtk支持rtk hook cursor子命令(rtk rewrite自 0.23.0 引入);遗留 shell 脚本路径则额外要求jq
  2. 全局作用域:安装/卸载都必须带--global,钩子注册在用户级~/.cursor/hooks.json,对所有 Cursor 会话生效,编辑器与 cursor-cli 共用;
  3. JSON 全路径契约:所有代码路径(含错误路径)都输出 JSON,这是与其他 Agent 钩子最大的协议差异,二次开发时不可“输出空字符串代替{}”;
  4. 权限模型不放大:RTK 只自动放行命中~/.cursor/cli-config.jsonShell(...)allow 规则的命令,其余改写一律走ask;对不可证明构造(命令替换、重定向、heredoc)一律透传不碰;
  5. 可观测性RTK_HOOK_AUDIT=1提供逐次改写的审计日志,rtk gain可查看 token 节省统计(节省比例数字来自仓库文档的标注区间,实际节省取决于命令类别与输出量)。

7. 小结

rtk 的 Cursor 集成展示了“单一事实源 + 多宿主薄适配”的工程设计:70+ 条改写规则集中在 src/discover/registry.rs,Cursor 侧仅需处理两件独有的事——permission/updated_input字段命名与“全路径输出 JSON(含{})”契约。原生实现 run_cursor() 通过 1 MiB stdin 上限、BOM 剥离、权限判定四档分流与审计日志,把“透明改写”做到既不改变用户工作流、也不放大宿主权限模型的程度;rtk-rewrite.sh 则作为兼容遗留安装的薄委派脚本保留。对希望在 Cursor Agent 会话中降低 LLM 上下文开销的开发者,rtk init --global --agent cursor一条命令即可完成接入。

【免费下载链接】rtkCLI proxy that reduces LLM token consumption by 60-90% on common dev commands. Single Rust binary, zero dependencies项目地址: https://gitcode.com/GitHub_Trending/rtk4/rtk

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询