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

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

2026-09-06 13:15:35作者:江焘钦

本文基于 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.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 集成有两个历史形态,本文都会覆盖:

  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.rsrewrite_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.rsload_cursor_rules():读取全局 ~/.cursor/cli-config.json 中的 permissions.denypermissions.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

  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 cursorinit.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 一条命令即可完成接入。

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