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 status→rtk 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.rs | rtk init/rtk uninstall的写入、补丁、备份与原子写逻辑 |
| 原生运行时 | src/hooks/hook_cmd.rs | rtk 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 集成有两个历史形态,本文都会覆盖:
- 原生二进制钩子(当前默认):
rtk init --global --agent cursor在~/.cursor/hooks.json中注册rtk hook cursor命令,由 Rust 二进制直接处理 stdin/stdout; - 遗留 shell 脚本:hooks/cursor/rtk-rewrite.sh,仓库中保留的薄委派脚本,依赖
jq和rtk >= 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 原生流程处理。
只有在判定为AllowRewrite或AskRewrite时才输出带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 MiBread_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_allowed:git 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 钩子同样生效:
RTK_DISABLED=1:环境变量前缀按命令禁用改写,例如RTK_DISABLED=1 git status原样执行。注册表在 env 前缀中检测到RTK_DISABLED=时跳过改写(registry.rs);exclude_commands:在~/.config/rtk/config.toml中列出永不改写的命令,支持子命令模式("git push"同时排除git push origin main)和^开头的正则模式;- 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.deny与permissions.allow数组,只识别Shell(前缀的规则并剥离包装(裸Shell规则映射为*)。源码注释解释了设计约束:只读全局配置,因为 RTK 的自动放行集合必须是宿主信任集合的子集(宿主还会叠加项目级配置与 folder-trust),RTK 绝不能比 Cursor 更宽松。
测试矩阵(hook_cmd.rs 测试区)覆盖了该集成的全部关键路径:
| 测试 | 验证点 |
|---|---|
test_cursor_rewrite_flat_format | allow 规则下git status输出扁平格式(无hookSpecificOutput包裹) |
test_cursor_default_verdict_rewrites | 无规则时默认判定走ask而非allow |
test_cursor_unallowed_segment_asks | git status && rm -rf /tmp/x中未授权段导致ask |
test_cursor_passthrough_empty_json | 未支持命令(htop)返回{} |
test_cursor_empty_input_empty_json | 空输入返回{} |
test_cursor_heredoc_passthrough | heredoc 命令透传返回{} |
test_cursor_deny_blocks_rewrite | deny 规则阻断改写 |
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:
jq缺失:警告并退出(jq是提取命令字段的唯一依赖);rtk不在 PATH:警告并退出;- 版本守卫:
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 cursorCursor 钩子是global-only:不带--global时rtk 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 cursorremove_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 钩子说明 与源码,使用该集成时需要注意以下边界:
- 版本要求:原生二进制钩子需要
rtk支持rtk hook cursor子命令(rtk rewrite自 0.23.0 引入);遗留 shell 脚本路径则额外要求jq; - 全局作用域:安装/卸载都必须带
--global,钩子注册在用户级~/.cursor/hooks.json,对所有 Cursor 会话生效,编辑器与 cursor-cli 共用; - JSON 全路径契约:所有代码路径(含错误路径)都输出 JSON,这是与其他 Agent 钩子最大的协议差异,二次开发时不可“输出空字符串代替
{}”; - 权限模型不放大:RTK 只自动放行命中
~/.cursor/cli-config.json中Shell(...)allow 规则的命令,其余改写一律走ask;对不可证明构造(命令替换、重定向、heredoc)一律透传不碰; - 可观测性:
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),仅供参考