RTK Hook 系统源码解析:从 rtk init 多 Agent 安装、SHA-256 完整性校验到 Deny > Ask > Allow 权限判决模型
【免费下载链接】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)的 hook 层负责让 AI 编程助手自动将原始 CLI 命令重写为 RTK 等价命令,从而在不依赖用户手动配置的前提下获得 token 节省。本文以仓库内 src/hooks/README.md 为骨架,结合 init.rs、integrity.rs、permissions.rs、rewrite_cmd.rs 等源码实现,系统讲解rtk init的安装模式、PatchMode 补丁策略、五态完整性校验、原子写入机制与跨 Agent 的权限判决模型,读完后可完整理解 RTK hook 的生命周期管理设计并具备为新增 Agent 编写安装逻辑的能力。
一、模块定位:生命周期管理层,而非重写执行层
src/hooks是整个 RTK 中面向 LLM agent 的 hook 生命周期管理层:负责 hook 的安装、卸载、完整性校验、使用审计与信任管理。一个关键的边界划分是——它创建并维护位于仓库根目录hooks/下的 hook 产物,但自身不执行重写逻辑,重写模式注册表在 discover/registry.rs,命令输出过滤在src/cmds/。
该模块明确拥有的职责包括:
rtk init安装流程(通过 main.rs#L39-L62 中的AgentTarget枚举驱动);- SHA-256 完整性校验与 hook 版本检查;
- 审计日志分析(
rtk hook-audit); rtk rewriteCLI 入口;- TOML 过滤器信任管理(
rtk trust)。
两条边界说明值得注意(引自 src/hooks/README.md):
- rewrite_cmd.rs 是一个薄 CLI 桥——它存在的唯一目的是服务 hook(hook 以子进程方式调用
rtk rewrite),并完全委托给discover/registry; - trust.rs 控制项目级 TOML 过滤器的执行许可。它放在 hooks 模块而非核心过滤引擎中,因为信任流程与"hook 安装时发现的过滤器"这一场景绑定。
模块的总入口是 src/hooks/mod.rs,其中is_claude_hook_command()用discover::lexer::shell_split解析命令,识别rtk hook claude形态(含绝对路径与带引号路径,单元测试见 mod.rs#L26-L45),保证 hook 命令无论以何种路径调用都能被正确识别。
二、核心目的:命令拦截与自动重写
hook 的工作模型是:AI 助手即将执行一条原始 CLI 命令(如git status)时,hook 拦截该命令,将其重写为 RTK 等价形式(如rtk git status),使 LLM agent 自动获得 token 节省收益。整个仓库的 hook 实现分为两类:
- 安装产物:部署到用户机器上的脚本/插件,位于根目录 hooks/(如 hooks/claude/rtk-rewrite.sh、hooks/opencode/rtk.ts、hooks/pi/rtk.ts);
- 原生 Rust hook 命令:constants.rs 定义了
rtk hook claude、rtk hook cursor、rtk hook droid、rtk hook vibe等原生命令,注释明确标注它们"replaces rtk-rewrite.sh",即新版安装直接注册二进制命令而非 shell 脚本。
三、rtk init:安装模式全景
rtk init支持以下安装流程(完整继承自文档,含各模式创建与修补的文件):
| 模式 | 命令 | 创建 | 修补 |
|---|---|---|---|
| Default (global) | rtk init -g | Hook, SHA-256 hash, RTK.md | settings.json, CLAUDE.md |
| Hook only | rtk init -g --hook-only | Hook, SHA-256 hash | settings.json |
| Claude-MD (legacy) | rtk init --claude-md | 134-line RTK block | CLAUDE.md |
| Windsurf | rtk init -g --agent windsurf | .windsurfrules | -- |
| Cline | rtk init --agent cline | .clinerules | -- |
| Codex | rtk init --codex | RTK.md in$CODEX_HOMEor~/.codex | AGENTS.md |
| Cursor | rtk init -g --agent cursor | Cursor hook | hooks.json |
| Pi | rtk init --agent pi | .pi/extensions/rtk.ts | -- |
| Hermes | rtk init --agent hermes | Python plugin in~/.hermes/plugins/rtk-rewrite/ | config.yamlplugins.enabled |
AgentTarget 枚举:源码中的实际覆盖面
文档描述安装流程"6 agents viaAgentTargetenum,现包括 Mistral Vibe + 3 个特殊模式(Gemini、Codex、OpenCode)"。从 main.rs#L39-L62 的当前源码看,AgentTarget枚举已扩展为 11 个变体:Claude(默认)、Cursor、Windsurf、Cline、Kilocode、Antigravity、Kimi、Pi、Hermes、Droid、Vibe,说明该枚举是随新 Agent 支持持续增长的注册表,而非固定清单。
init 流程的源码要点
init.rs 中值得关注的实现细节:
- 嵌入产物:OpenCode 插件与 Pi 扩展通过
include_str!在编译期嵌入 hooks/opencode/rtk.ts 与 hooks/pi/rtk.ts,RTK 感知指令则嵌入 hooks/claude/rtk-awareness.md(Claude 与 Codex 各有一份精简版),因此安装不依赖仓库目录存在; - 过滤器模板:当目标位置没有
filters.toml时,rtk init会写入带注释的模板(FILTERS_TEMPLATE/FILTERS_GLOBAL_TEMPLATE),示例展示了match_command、strip_lines_matching、max_lines、on_empty等字段用法; - 参数冲突校验:
run()入口先做模式互斥检查,例如--codex不能与--opencode、--claude-md、--hook-only、--auto-patch、--no-patch组合;OpenCode、Cursor、Windsurf 模式是 global-only,缺少-g会直接报错并给出正确用法提示(见 init.rs#L278-L308)。
四、完整性校验:防止 hook 被篡改
完整性系统的动机在 integrity.rs#L1-L13 的模块注释中写明:RTK 安装的 PreToolUse hook 会以permissionDecision: "allow"自动批准重写后的命令,绕过了 Claude Code 的权限提示,因此任何未授权的 hook 修改都构成命令注入向量。系统流程分三步:
- 安装时:
integrity::store_hash()计算 hook 文件的 SHA-256,写入~/.claude/hooks/.rtk-hook.sha256(只读 0o444); - 运行时:
integrity::runtime_check()重新计算哈希并比对,若被篡改则阻止执行; - 按需:
rtk verify打印详细校验状态(PASS/FAIL/WARN/SKIP)。
五种完整性状态定义在 integrity.rs#L28-L39 的IntegrityStatus枚举中:
| 状态 | 含义 |
|---|---|
| Verified | 哈希与存储值一致 |
| Tampered | 哈希不匹配(阻止执行) |
| NoBaseline | hook 存在但无存储哈希(旧版安装) |
| NotInstalled | 无 hook、无哈希 |
| OrphanedHash | 哈希文件存在但 hook 缺失 |
源码层面还有两处值得注意的工程取舍(见 integrity.rs#L79-L108 的store_hash):
- 哈希文件格式兼容
sha256sum -c:<hex_hash> rtk-rewrite.sh,可用标准工具复核; - 0o444 只读权限自述为"speed bump 而非安全边界"——拥有写权限的攻击者可以 chmod 掉它,其价值在于把"意外覆盖"变成"需要刻意为之的动作"。这种对安全模型边界的诚实标注,是阅读该源码时值得借鉴的做法。
五、PatchMode:settings 文件的三种修补策略
rtk init修改 agent 配置文件(如settings.json)时的行为由 PatchMode 控制,定义在 init.rs#L78-L84:
| 模式 | 标志 | 行为 |
|---|---|---|
| Ask (default) | -- | 提示用户[y/N];stdin 非终端时默认 No |
| Auto | --auto-patch | 不提示直接修补;面向 CI/脚本化安装 |
| Skip | --no-patch | 打印手工操作说明,由用户自行修补 |
与修补相关的完整状态由PatchResult枚举表达(init.rs#L94-L102):Patched、AlreadyPresent(hook 已在 settings.json 中)、Declined(用户拒绝提示)、Skipped(--no-patch)、WouldPatch(dry-run 下本应添加)。所有 init 子模式共享InitContext { verbose, dry_run }上下文,dry-run 模式只打印"would create / would update"而不触碰文件系统,结束后统一输出[dry-run] Nothing written.尾注——这让安装流程可以在不落盘的情况下被完整验证。
过滤器信任也有对等的三态FilterTrust { Ask, Trust, Skip }(默认 Ask,见 init.rs#L86-L92),与 PatchMode 形成"修补设置文件"与"信任项目过滤器"两条并列的交互式决策链。
六、原子性与幂等性
所有文件操作使用原子写入(tempfile + rename)防止崩溃时产生半截文件;settings 类文件在修改前备份为.bak;全部操作幂等——多次运行rtk init安全无副作用。init.rs 中的write_if_changed()进一步实现了"内容未变则不写盘"(verbose 下打印 already up to date),并对 symlink 目标做了显式处理,保证 rename 落在真实文件上、symlink 本身被保留。
七、权限模型:Deny > Ask > Allow
RTK 强制执行一套与 Claude Code 最小权限默认值对齐的权限优先级:
Deny > Ask > Allow (explicit) > Default (ask)规则来源与解析规则:
- 从所有Claude Code
settings.json文件加载(项目 + 全局,含.local变体,路径常量见 constants.rs 的SETTINGS_JSON/SETTINGS_LOCAL_JSON); - 只提取
Bash(...)规则,其他作用域(Read、Write)被忽略。
四种判决到 hook 行为的映射(完整继承自文档):
| 判决 | 触发条件 | rewrite_cmd 退出码 | Hook 行为 |
|---|---|---|---|
| Deny | permissions.deny规则匹配 | 2 | Passthrough — 交由宿主工具处理拒绝 |
| Ask | permissions.ask规则匹配 | 3 | 重写 + 让宿主工具提示用户 |
| Allow | permissions.allow规则匹配 | 0 | 重写 + 自动放行 |
| Default | 无规则匹配 | 3 | 重写 + 让宿主工具提示用户 |
判决逻辑的源码级拆解
permissions.rs 的check_command_with_rules()实现了三条比"简单优先级"更严格的安全规则:
- Deny 抢占:任一命令段匹配 deny 规则立即返回
Deny,先于一切其他构造; - 不可证伪构造强制 Ask:若命令包含
contains_unattestable_construct(如命令替换、文件目标重定向等无法静态证明安全的构造),直接降级为 Ask,永不自动放行; - Allow 要求全段匹配:复合命令(
&&链)中每一个非空段都必须独立命中 allow 规则才给 Allow,任一段不匹配即失去 Allow 状态。源码注释指明这是 issue #1213 的修复——此前单个段命中 allow 就升级整条链为 Allow,可被用于绕过。
退出码契约与 rewrite_cmd
rewrite_cmd.rs#L12-L17 的文档注释给出了 hook 消费侧看到的完整契约:
| 退出码 | stdout | 含义 |
|---|---|---|
| 0 | 重写结果 | 重写被允许 — hook 可自动放行重写命令 |
| 1 | (空) | 无 RTK 等价命令 — hook 原样透传 |
| 2 | (空) | Deny 规则命中 — 交由 Claude Code 原生拒绝处理 |
| 3 | 重写结果 | Ask 规则命中 — 重写但让 Claude Code 提示用户 |
hook 中的典型消费方式即文档给出的模式:REWRITTEN=$(rtk rewrite "$CMD") || exit 0。注意rewrite_cmd.rs中Allow走正常Ok(())返回,而Ask/Deny/Passthrough通过std::process::exit直接退出——退出码本身就是协议。
各 Agent 的 ask 支持矩阵
| 工具 | ask 支持 | Default 下的行为 |
|---|---|---|
| Claude Code (rtk-rewrite.sh) | Yes | permissionDecision: "ask"— 提示用户 |
| Copilot VS Code (rtk hook copilot) | Yes | permissionDecision: "ask"— 提示用户 |
| Cursor (rtk hook cursor) | Ready | permission: "ask"— Cursor 实施该权限后将提示用户;在此之前放行 |
| Gemini CLI (rtk hook gemini) | No(仅 allow/deny) | allow(限制 — Gemini 无 ask 模式) |
| Copilot CLI (rtk hook copilot) | No updatedInput | deny-with-suggestion(行为不变) |
| Codex | ask 被解析但为 no-op | allow(限制 — fails open) |
| Mistral Vibe (rtk hook vibe) | 无原生 ask 界面 | passthrough — Vibe 自身对重写命令触发审批提示 |
对应地,permissions.rs#L34-L41 的Host枚举按宿主加载各自的规则来源:Claude(Claude settings.json)、Cursor(.cursor目录)、Gemini(.gemini目录)、Droid(.factory目录 +FACTORY_HOME_OVERRIDE)、Vibe(当前为空规则集,即完全 passthrough 语义)。
实现分工
- permissions.rs — 加载 deny/ask/allow 规则、评估优先级、返回
PermissionVerdict; - rewrite_cmd.rs — 将判决映射为退出码(shell hook 消费);
- hook_cmd.rs — 将判决映射为 JSON
permissionDecision字段(Copilot/Gemini 等 stdin/stdout JSON 协议消费),main.rs#L899-L921 的HookCommands枚举登记了Claude、Cursor、Gemini、Copilot、Droid、Vibe六个处理器及一个Check干跑入口(rtk hook check --agent <agent> <command>)。
八、非阻塞保证:Exit Code Contract
hook_cmd.rs 中的 hook 处理器必须在每一条路径上都返回Ok(())——成功、无匹配、解析错误乃至意外输入皆然。返回Err会传播到main()导致非零退出,从而阻塞 agent 命令的执行,违反 hooks/README.md 中记载的非阻塞保证。这是 hook 类组件的核心不变量:hook 是旁路加速器,绝不能成为命令执行的故障点。模块上#[deny(clippy::print_stdout, clippy::print_stderr)](见 mod.rs#L6)也侧面印证了该模块对输出通道的严格纪律——JSON 协议处理器不允许有额外输出污染 stdout。
九、配套机制:信任管理、版本检查与审计
文档将信任管理列为本模块职责之一,trust.rs 的模块注释给出了完整的trust-before-load模型:.rtk/filters.toml从 CWD 以最高优先级加载,攻击者可将其提交到公共仓库来控制 LLM 看到的内容(隐藏恶意代码、压制安全扫描输出、用replace/match_output原语改写命令输出)。为此:未信任的过滤器被直接跳过而非"带警告加载";rtk trust在用户审查后存储 SHA-256;内容变化即失效信任(需重新审查);RTK_TRUST_PROJECT_FILTERS=1供 CI 流水线覆盖。
另外两个文档点名的能力在源码中同样可查证:
- hook 版本检查:hook_check.rs 中
CURRENT_HOOK_VERSION = 3,maybe_warn()每 24 小时至多提示一次,HookStatus枚举区分Ok/Outdated/Missing,并能识别"新二进制 hook 已注册但旧脚本未清理"的半迁移状态,提示用户重跑rtk init -g清理; - 审计:
rtk hook-audit --since N(默认 7 天)展示 hook 重写指标,前提是环境变量RTK_HOOK_AUDIT=1(见 main.rs#L869-L875 的命令定义与 hook_audit_cmd.rs)。
十、开发指南:接入一个新的 AI 编程 Agent
文档给出的四步扩展流程与源码结构一一对应,是贡献该模块的标准路径:
- 安装逻辑:在 init.rs 中按现有 agent 模式添加安装函数(可参考 embedded
include_str!插件与write_if_changed原子写入的既有写法); - 自定义 hook 协议处理器:若 agent 需要专属协议(如 Gemini 的
BeforeTool或 Vibe 的pre_tool事件键,常量见 constants.rs 的PRE_TOOL_USE_KEY/BEFORE_TOOL_KEY),在 hook_cmd.rs 添加处理器函数,并在 main.rs 中同步添加HookCommands::<Agent>变体与AgentTarget::<Agent>枚举项; - 权限面接线:若 agent 有可安装的权限面(denylist/allowlist),在 permissions.rs 的
check_command_for()中通过新的Host::<Agent>变体接入; - 完整性基线:在 integrity.rs 中为新 hook 文件登记期望哈希。
两条注意事项:hook_check.rs::maybe_warn()目前只检查 Claude Code hook,其他 agent 没有过期 hook 的警告路径;验证方式是在全新环境中运行rtk init,然后在目标 agent 中确认 hook 能正确重写命令。
十一、相关文件索引
| 文件 | 职责 |
|---|---|
| src/hooks/mod.rs | 模块入口、is_claude_hook_command识别 |
| src/hooks/init.rs | 各 agent 安装流程、PatchMode、dry-run、模板 |
| src/hooks/integrity.rs | SHA-256 存储/校验、五态IntegrityStatus |
| src/hooks/permissions.rs | deny/ask/allow 规则加载与判决 |
| src/hooks/rewrite_cmd.rs | rtk rewrite退出码契约 |
| src/hooks/hook_cmd.rs | 各 agent 的 JSON 协议处理器 |
| src/hooks/hook_check.rs | hook 版本检查与过期警告 |
| src/hooks/verify_cmd.rs | rtk verify:TOML 过滤器内联测试执行 |
| src/hooks/trust.rs | 项目过滤器信任存储 |
| src/hooks/hook_audit_cmd.rs | hook 重写审计指标 |
| src/hooks/constants.rs | 目录、事件键、hook 命令常量 |
| hooks/README.md | 部署产物(安装脚本/插件)总览 |
| docs/contributing/TECHNICAL.md | 整体架构参考 |
需要说明的适用前提:本文以当前仓库src/hooks的源码状态为准;AgentTarget枚举成员、Host变体与各 agent 的 ask 支持矩阵会随新 Agent 接入而变化,接入新 agent 前建议先核对 main.rs 与 permissions.rs 的最新定义。
【免费下载链接】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),仅供参考