rtk 的 Cursor 集成实战:preToolUse 钩子如何透明改写 Agent 命令以削减 Token 消耗
本文基于 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 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_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 cursor
Cursor 钩子是 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 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 钩子说明 与源码,使用该集成时需要注意以下边界:
- 版本要求:原生二进制钩子需要
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 一条命令即可完成接入。
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 StartedRust0624
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