claw-code 原生 Git 上下文工具:GitStatus / GitDiff / GitLog / GitShow / GitBlame 五个工具的设计与源码解析
在 claw-code 的 Rust 工具层(rust/crates/tools)中,仓库为模型提供了五个原生的只读 Git 上下文工具,用结构化、可发现的工具定义替代了通过 bash 拼凑临时 git 命令的旧模式。本文以 rust/crates/tools/GIT_TOOLS_README.md 为主体,完整覆盖五个工具的全部参数、输入输出示例与使用场景,并结合 工具 crate 源码 中真实的参数拼装逻辑、权限模型与测试用例,说明每个工具在底层是如何构造 git 命令、处理错误以及受权限门控约束的。读完本文,你既能直接按文档使用这五个工具,也能理解它们的实现边界与限制。
为什么需要原生 Git 工具
在引入这组工具之前,模型若要读取仓库状态,只能通过 bash 工具执行 ad-hoc 的 git 命令。文档明确指出这存在四个问题:
- 无结构化输出:Bash 返回原始文本,需要模型自行解析;
- 权限过宽:即使是只读的
git status,Bash 也需要DangerFullAccess级别权限; - 不可发现:模型无法通过
ToolSearch检索到具备 git 能力的工具; - 不一致:每次调用可能使用不同的参数与格式。
换成原生工具后,五个工具全部声明为 ReadOnly 权限模式,在受限权限模式下安全可用;输出为结构化的 JSON;可通过 ToolSearch 用 "git"、"diff"、"blame" 等关键词发现;每个工具的描述文字都会向模型解释何时应优先于 bash 使用。这些声明均可在源码的静态工具表中逐一印证——五个 ToolSpec 的 required_permission 字段全部为 PermissionMode::ReadOnly,见 mvp_tool_specs 中的 Git 工具定义。
架构:五工具统一的四层实现模式
文档将五个工具的共同架构归纳为四个层次,这一模式与源码完全一致:
- ToolSpec —— 定义工具名、描述、JSON 输入 schema 以及
PermissionMode::ReadOnly。在源码中,五个工具依次注册于 工具静态表。值得注意的是,每个 schema 都带有"additionalProperties": false,即严格拒绝未声明的多余字段,这是比文档更进一步的输入契约; - Input struct —— 派生
Deserialize,可选字段使用#[serde(default)]。五个对应的结构体定义于 输入结构体区块,例如GitStatusInput只有一个可选的short: Option<bool>字段; - Run 函数 —— 拼装
git参数、调用git_stdout()辅助函数、再用to_pretty_json()将结果包装为 JSON。实现位于 run_git_* 函数区; - Dispatch —— 与其他工具一样在
execute_tool_with_enforcer()中按工具名匹配分发,见 分发匹配块:
"GitStatus" => from_value::<GitStatusInput>(input).and_then(run_git_status),
"GitDiff" => from_value::<GitDiffInput>(input).and_then(run_git_diff),
"GitLog" => from_value::<GitLogInput>(input).and_then(run_git_log),
"GitShow" => from_value::<GitShowInput>(input).and_then(run_git_show),
"GitBlame" => from_value::<GitBlameInput>(input).and_then(run_git_blame),
分发前的权限校验由 enforce_permission_check 完成:每次工具调用都会经过权限门控,被拒绝时返回带原因的错误。按照 tools crate 的 AGENTS.md 约定,所有工具分发都不得绕过权限检查,工具边界签名统一为 Result<String, String>。
git_stdout 辅助函数
所有五个工具最终都委托给同一个辅助函数 git_stdout(args: &[&str]) -> Option<String>,其语义值得注意:
fn git_stdout(args: &[&str]) -> Option<String> {
let output = Command::new("git").args(args).output().ok()?;
if !output.status.success() {
return None;
}
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
(!stdout.is_empty()).then_some(stdout)
}
它负责启动 git 子进程,返回去除首尾空白后的 stdout;当 git 进程启动失败、退出码非零、或 stdout 为空时返回 None。因此每个 run_git_* 函数在拿到 None 时都会返回针对该命令定制的中文错误提示,例如 "git status failed. Ensure the current directory is inside a git repository."。这意味着工具成功执行时始终返回 {"output": "..."} 这样的 JSON 对象,失败时返回可诊断的错误字符串,而不是空值。
GitStatus:查看工作区状态
展示工作树状态(分支、已暂存、未暂存、未跟踪文件),等价于 git status --short --branch。
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
short |
boolean | 否 | true |
使用 --short --branch 格式输出简洁结果 |
示例输入:
{}
示例输出:
{
"output": "## feat/git-aware-tools...upstream/main [ahead 1]\nM rust/crates/tools/src/lib.rs"
}
源码实现见 run_git_status:当 short 为 true(未提供时也按 true 处理)时追加 --short --branch;显式传 false 则回退为完整的 git status 输出。
GitDiff:查看差异
展示提交、索引与工作树之间的变化,支持已暂存变更、指定路径、提交范围以及两个提交之间的比较。
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
staged |
boolean | 否 | false |
显示已暂存变更(git diff --cached) |
commit |
string | 否 | — | 与之对比的提交哈希、标签或分支 |
commit2 |
string | 否 | — | 范围对比的第二个提交(commit...commit2) |
path |
string | 否 | — | 限定 diff 到某个文件路径 |
示例输入:
{}
{ "staged": true }
{ "commit": "HEAD~3", "path": "rust/crates/tools/src/lib.rs" }
{ "commit": "main", "commit2": "feat/git-aware-tools" }
从 run_git_diff 的参数拼装 可以看到几个精确行为:
staged为true时插入--cached;commit与commit2同时提供时,拼成"{commit}...{commit2}"三点范围语法;只提供commit时直接作为对比基线;path存在时会插入--分隔符再跟路径,避免以-开头的文件名被误解析为选项。
GitLog:查看提交历史
展示提交历史,支持数量限制、按作者/日期/路径过滤以及 oneline 格式,默认返回最近 20 条提交。
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
count |
integer | 否 | 20 |
返回的最大提交数 |
oneline |
boolean | 否 | false |
使用 --oneline 格式(仅哈希 + 标题) |
author |
string | 否 | — | 按作者模式过滤提交 |
since |
string | 否 | — | 起始日期过滤(如 "2024-01-01" 或 "2.weeks") |
until |
string | 否 | — | 截止日期过滤 |
path |
string | 否 | — | 按文件或目录路径过滤提交 |
示例输入:
{ "count": 5, "oneline": true }
{ "author": "alice", "since": "1.week", "path": "src/main.rs" }
源码层面(run_git_log),count 会拼成 -n{count} 形式追加到参数最前,author/since/until 分别对应 --author=、--since=、--until=;path 同样以 -- 分隔。工具 schema 中还声明了 "count": { "type": "integer", "minimum": 1 },保证至少返回一条记录。
GitShow:查看提交、标签或树对象
展示某个提交、标签或树对象及其 diff,支持查看某提交下的特定文件(commit:path 语法),并支持仅显示统计信息。
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
commit |
string | 是 | — | 要显示的提交哈希、标签或分支引用 |
path |
string | 否 | — | 仅显示该提交下的此文件(commit:path 语法) |
stat |
boolean | 否 | false |
显示 diffstat 摘要而非完整 diff |
示例输入:
{ "commit": "HEAD" }
{ "commit": "abc1234", "stat": true }
{ "commit": "main", "path": "src/lib.rs" }
源码实现 run_git_show 比文档表格更进一步:当前代码在 stat 之外还引入了 format 参数(取值为 "patch"(默认)、"stat"、"metadata",见 GitShow 的 ToolSpec schema),其语义为:
format: "metadata"会追加--format=medium --no-patch,只输出提交元信息而不含 diff;format一旦提供即优先于stat布尔值,stat: true被保留为向后兼容的旧式写法;- 传入未知取值会返回明确错误:
unknown GitShow format: "..."; metadata格式不能与path组合——metadata 描述的是提交对象而非某个 blob,源码直接拒绝该组合并提示改用patch/stat或省略path;- 提供
path时,工具自动拼为{commit}:{path}语法传给git show。
这些行为均有对应测试覆盖,例如 git_show 相关测试用例 验证了 schema 暴露 format 枚举、patch/stat/metadata 三种格式均能执行、stat: true 的旧式写法兼容、以及非法格式值会返回 "unknown GitShow format" 错误。
GitBlame:逐行溯源
展示文件的每一行最近一次由哪个修订和哪个作者修改,支持行范围过滤。
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
path |
string | 是 | — | 要 blame 的文件路径 |
start_line |
integer | 否 | — | 行范围起点(1 基) |
end_line |
integer | 否 | — | 行范围终点(1 基) |
示例输入:
{ "path": "src/main.rs" }
{ "path": "src/main.rs", "start_line": 100, "end_line": 150 }
从 run_git_blame 的实现看,行范围会拼为 git 原生的 -L{start},{end} 选项;一个文档未明说的细节是:start_line 与 end_line 必须同时提供才会生效(源码中用 if let (Some(start), Some(end)) 配对判断),只提供其一会被静默忽略,因此实际使用时应成对传入。schema 层同样以 minimum: 1 约束行号从 1 开始。
权限模型与分发链路
每个工具被调用时,完整链路是:模型发出带 JSON 输入的工具调用 → execute_tool_with_enforcer() 按工具名匹配分发 → 分发前先经 PermissionEnforcer 做权限检查(enforce_permission_check 将检查结果映射为 Ok 或携带拒绝原因的 Err)→ from_value 将 JSON 反序列化为对应的 Input struct(多余字段因 additionalProperties: false 被 schema 拒绝)→ run_git_* 拼装参数并调用 git_stdout() → 成功时经 to_pretty_json 返回 {"output": ...},失败时返回包含操作上下文的错误消息。
由于五个工具都是 ReadOnly 权限,它们可以在受限权限模式下直接运行;这与 bash 中即使是只读 git 命令也需要更高权限形成对比。对照 tools crate 的 AGENTS.md 中记录的约定:所有工具分发都必须经过权限门控,工具边界签名统一为 Result<String, String>,测试命名遵循 BDD 风格的 given_x_when_y_then_z。
构建与测试验证
文档给出的验证命令如下:
cd rust
cargo build --release
cargo test -p tools
其中 cargo test -p tools 针对 tools crate 运行全部测试,包括上节提到的 GitShow format 枚举、三种输出格式、legacy stat 兼容与非法格式拒绝等用例。文档同时说明:该 crate 中存在 3 个与本次改动无关的既有测试失败(agent_fake_runner、agent_persists_handoff、worker_create_merges_config),原因是本地 settings.json 配置不兼容,判断测试结果时应将这 3 个失败排除在外。
使用前提与限制
综合文档与源码,使用这五个工具时需要注意以下边界:
- 工作目录必须在 git 仓库内:
git_stdout()基于子进程的 stdout 判断成败,不在仓库内执行时各工具会返回形如 "Ensure the current directory is inside a git repository" 的错误提示,而不是静默返回空; - 空结果视为失败:
git_stdout()在 stdout 为空时返回None,因此"命令成功但没有可显示内容"(例如git diff无差异)会以错误形式返回,调用方需要理解这一语义; - 严格只读:五个工具只覆盖
status/diff/log/show/blame等只读子命令,且 schema 拒绝任何未声明参数,无法借此执行写操作; GitShow的format与path存在组合约束:metadata格式不可与path同时使用;GitBlame行范围必须成对提供:start_line和end_line单独提供不生效。
这五个工具把最常用的仓库只读查询收敛为模型可直接发现、可直接调用、权限最小化的结构化接口,其参数设计与错误处理均可在 rust/crates/tools/src/lib.rs 中逐行对照验证,适合作为在 Agent 工具层中集成领域命令的典型参考。
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 StartedRust0622
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