Skip to content

Commit 676ddc3

Browse files
committed
feat: add bounded Claude post-tool feedback adapter
1 parent 65758e5 commit 676ddc3

14 files changed

Lines changed: 487 additions & 28 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ Supply the original checker's required tool/configuration options to `task verif
160160
| `plan CATEGORY LANGUAGE` | Preview selections and gaps | Not a certified execution plan |
161161
| `hook plan` | Route a versioned host event to a candidate check tier | Reads bounded JSON on stdin; exit 3, no check or host blocking |
162162
| `hook execute` | Run read-only discovery, bounded Stop guidance, task-bound recheck, selected Python Ruff feedback, or a live pre-commit index safety preview | Explicit timeout; repair reuses `task verify` and never closes a task or approves delivery |
163+
| `hook claude post-tool-use` | Convert a successful Claude Code file-tool event into bounded Rust edit feedback | Candidate soft Hook; no plugin runtime binding or delivery gate yet |
163164
| `lint python / java / typescript / go` | Run selected native checks | Adapter-specific options and scope |
164165
| `comments rust`, `build rust` | Documentation and type checking | Build does not run project tests |
165166
| `cve rust / python / typescript` | Native advisory observations | Database identity/freshness and full coverage remain limited |

‎README.zh-CN.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ codeguard task verify "$TASK_ID" . --format json
159159
| `tools list / verify`、`doctor` | 查看工具和有限环境探测 | 显式 Ruff doctor 探测;`tools install --apply` 受阻 |
160160
| `plan CATEGORY LANGUAGE` | 预览选择和缺口 | 不是已认证执行计划 |
161161
| `hook execute` | 启动只读发现、Stop 有界下一步、按任务原工具复检、Python 编辑局部反馈及显式 Git 工具的提交面安全预览 | 必须给超时;复检不自动关闭任务,推送/CI 仍未接线,不构成宿主交付门禁 |
162+
| `hook claude post-tool-use` | 将 Claude Code 成功文件工具事件转换为有界 Rust 编辑反馈 | 候选软 Hook;尚无插件二进制绑定或交付门禁 |
162163
| `lint python / java / typescript / go` | 执行已接入原生检查 | 参数和范围因适配器而异 |
163164
| `comments rust`、`build rust` | 文档与类型检查 | build 不运行项目测试 |
164165
| `cve rust / python / typescript` | 原生漏洞公告观察 | 漏洞库身份、时效及完整覆盖仍有限 |
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
//! Claude Code 文件工具事件到 Rust 快检的受限转换;只回传脱敏对话摘要。
2+
3+
use crate::hook_execute_command::execute_host_input;
4+
use codeguard_core::{HookEvent, HookTriggerInput, HookWriteOutcome};
5+
use serde_json::{Value, json};
6+
use std::fs;
7+
use std::io::Read;
8+
use std::path::{Path, PathBuf};
9+
use std::process::ExitCode;
10+
11+
const MAX_HOST_INPUT_BYTES: u64 = 1024 * 1024;
12+
const MAX_CONTEXT_CHARS: usize = 1200;
13+
14+
/// 从 Claude Code stdin 读取成功的文件工具事件并注入局部检查摘要。
15+
/// 参数包含项目根和原 `hook execute` 的显式选项;返回宿主退出码而非质量结论。
16+
pub fn run(args: &[String]) -> ExitCode {
17+
let [event, rest @ ..] = args else {
18+
eprintln!("需要 Claude Code 事件与项目路径");
19+
return ExitCode::from(2);
20+
};
21+
if event != "post-tool-use" || rest.first().is_none_or(|value| value.starts_with('-')) {
22+
eprintln!("目前仅支持 post-tool-use PATH --timeout DURATION --format=json");
23+
return ExitCode::from(2);
24+
}
25+
let root = match Path::new(&rest[0]).canonicalize() {
26+
Ok(root) if root.is_dir() => root,
27+
_ => {
28+
print_context("范围未确定:项目根不可读取;本次源码检查未运行,交付未评估。");
29+
return ExitCode::SUCCESS;
30+
}
31+
};
32+
let mut raw = Vec::new();
33+
if std::io::stdin()
34+
.lock()
35+
.take(MAX_HOST_INPUT_BYTES + 1)
36+
.read_to_end(&mut raw)
37+
.is_err()
38+
|| raw.is_empty()
39+
|| raw.len() as u64 > MAX_HOST_INPUT_BYTES
40+
{
41+
print_context("范围未确定:宿主事件缺失或超出输入预算;本次源码检查未运行,交付未评估。");
42+
return ExitCode::SUCCESS;
43+
}
44+
let host = match codeguard_adapters::parse_unique_json(&raw) {
45+
Ok(host) => host,
46+
Err(_) => {
47+
print_context("范围未确定:宿主事件格式无效;本次源码检查未运行,交付未评估。");
48+
return ExitCode::SUCCESS;
49+
}
50+
};
51+
let path = match selected_file(&root, &host) {
52+
Some(path) => path,
53+
None => {
54+
print_context(
55+
"范围未确定:成功编辑事件没有可核对的项目内普通文件;本次源码检查未运行,交付未评估。",
56+
);
57+
return ExitCode::SUCCESS;
58+
}
59+
};
60+
let input = HookTriggerInput {
61+
event: HookEvent::FileChanged,
62+
changed_paths: vec![path.clone()],
63+
task_id: None,
64+
write_outcome: HookWriteOutcome::Confirmed,
65+
host_claims_blocking: false,
66+
};
67+
match execute_host_input(rest, &input) {
68+
Ok((report, _)) => print_context(&summarize(&path, &report)),
69+
Err(_) => {
70+
print_context("CodeGuard:宿主事件参数或路由无效;本次源码检查未运行,交付未评估。")
71+
}
72+
}
73+
ExitCode::SUCCESS
74+
}
75+
76+
fn selected_file(root: &Path, host: &Value) -> Option<String> {
77+
if host["hook_event_name"] != "PostToolUse"
78+
|| !matches!(
79+
host["tool_name"].as_str(),
80+
Some("Write" | "Edit" | "MultiEdit")
81+
)
82+
{
83+
return None;
84+
}
85+
let cwd = Path::new(host["cwd"].as_str()?);
86+
if !cwd.is_absolute() || !cwd.canonicalize().ok()?.starts_with(root) {
87+
return None;
88+
}
89+
let path = PathBuf::from(host["tool_input"]["file_path"].as_str()?);
90+
if !host["tool_response"].is_object() {
91+
return None;
92+
}
93+
if !path.is_absolute() || !fs::symlink_metadata(&path).ok()?.file_type().is_file() {
94+
return None;
95+
}
96+
if let Some(response_path) = host["tool_response"]["filePath"].as_str() {
97+
if response_path != path.to_str()? {
98+
return None;
99+
}
100+
}
101+
let canonical = path.canonicalize().ok()?;
102+
let relative = canonical.strip_prefix(root).ok()?.to_str()?;
103+
if relative.is_empty() || relative.contains('\\') || relative.chars().any(char::is_control) {
104+
return None;
105+
}
106+
Some(relative.to_owned())
107+
}
108+
109+
fn summarize(path: &str, report: &Value) -> String {
110+
let label = if path.len() <= 128
111+
&& path
112+
.bytes()
113+
.all(|byte| byte.is_ascii_alphanumeric() || b"._/-".contains(&byte))
114+
{
115+
path
116+
} else {
117+
"<changed-file>"
118+
};
119+
if report["execution"] != "local_observation" {
120+
let reason = report["reason"]
121+
.as_str()
122+
.unwrap_or("local_check_unavailable");
123+
return format!(
124+
"CodeGuard:{label} 的局部检查未运行({reason});请按项目原生检查要求复检,交付未评估。"
125+
);
126+
}
127+
let files = report["local_feedback"]["files"].as_array();
128+
let incomplete = report["local_feedback"]["local_scan_complete"] != true;
129+
let mut rules = Vec::new();
130+
if let Some(files) = files {
131+
for finding in files.iter().flat_map(|file| {
132+
file["findings"]
133+
.as_array()
134+
.into_iter()
135+
.flat_map(|findings| findings.iter())
136+
}) {
137+
let Some(rule) = finding["rule_id"].as_str() else {
138+
continue;
139+
};
140+
if rule.len() <= 32
141+
&& rule
142+
.bytes()
143+
.all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit())
144+
{
145+
rules.push(rule.to_owned());
146+
if rules.len() == 3 {
147+
break;
148+
}
149+
}
150+
}
151+
}
152+
let count = files.map_or(0, |files| {
153+
files
154+
.iter()
155+
.map(|file| file["findings"].as_array().map_or(0, Vec::len))
156+
.sum::<usize>()
157+
});
158+
let status = if incomplete {
159+
"局部检查未完成"
160+
} else {
161+
"局部检查已运行"
162+
};
163+
let detail = if count == 0 {
164+
"未取得可展示的原生诊断".to_owned()
165+
} else {
166+
format!("原生诊断 {count} 项;规则 {}", rules.join(", "))
167+
};
168+
let summary = format!(
169+
"CodeGuard:{label} {status},{detail}。请核对原生报告并在修复后复检;完整项目与交付未评估。"
170+
);
171+
summary.chars().take(MAX_CONTEXT_CHARS).collect()
172+
}
173+
174+
fn print_context(context: &str) {
175+
println!(
176+
"{}",
177+
json!({
178+
"hookSpecificOutput": {
179+
"hookEventName":"PostToolUse",
180+
"additionalContext":context
181+
},
182+
"systemMessage":"CodeGuard 局部检查反馈"
183+
})
184+
);
185+
}

‎crates/codeguard-cli/src/hook_execute_command.rs‎

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::python_lint_command::{
99
annotate_conversation_budget, scan_selected_report_with_deadline,
1010
};
1111
use codeguard_adapters::legacy_registry;
12-
use codeguard_core::{HookTriggerAction, plan_hook_trigger};
12+
use codeguard_core::{HookTriggerAction, HookTriggerInput, plan_hook_trigger};
1313
use codeguard_runtime::{NativeObservation, ProcessSpec, Termination, run_process};
1414
use serde_json::{Value, json};
1515
use std::collections::BTreeMap;
@@ -72,16 +72,34 @@ pub fn run(args: &[String]) -> ExitCode {
7272
return ExitCode::from(2);
7373
}
7474
};
75-
let plan = match plan_hook_trigger(&input) {
76-
Ok(plan) => plan,
75+
let (report, exit_code) = match execute_parsed(&arguments, &input) {
76+
Ok(result) => result,
7777
Err(reason) => {
7878
eprintln!("无效 hook 事件:{reason}");
7979
return ExitCode::from(2);
8080
}
8181
};
82+
println!("{report}");
83+
ExitCode::from(exit_code)
84+
}
85+
86+
/// 使用同一事件执行器处理已由宿主适配器规范化的输入。
87+
/// 参数为原 `hook execute` 选项和受限事件;返回原始 Rust 报告与其 CLI 退出码。
88+
pub(crate) fn execute_host_input(
89+
args: &[String],
90+
input: &HookTriggerInput,
91+
) -> Result<(Value, u8), String> {
92+
let arguments = parse_args(args)?;
93+
execute_parsed(&arguments, input).map_err(str::to_owned)
94+
}
95+
96+
fn execute_parsed(
97+
arguments: &Arguments,
98+
input: &HookTriggerInput,
99+
) -> Result<(Value, u8), &'static str> {
100+
let plan = plan_hook_trigger(input)?;
82101
if plan.action != HookTriggerAction::VerifyTask && !arguments.verify_options.is_empty() {
83-
eprintln!("任务复检参数仅用于 repair_ready 事件");
84-
return ExitCode::from(2);
102+
return Err("任务复检参数仅用于 repair_ready 事件");
85103
}
86104
let root = arguments
87105
.root
@@ -103,7 +121,7 @@ pub fn run(args: &[String]) -> ExitCode {
103121
},
104122
(HookTriggerAction::VerifyTask, Some(root)) => {
105123
let task_id = plan.task_id.as_deref().expect("核心已校验任务 ID");
106-
match task_verification_summary(root, task_id, &arguments) {
124+
match task_verification_summary(root, task_id, arguments) {
107125
Ok(feedback) => ("task_verification", None, feedback, 3),
108126
Err((reason, exit_code)) => ("not_run", Some(reason), Value::Null, exit_code),
109127
}
@@ -207,16 +225,15 @@ pub fn run(args: &[String]) -> ExitCode {
207225
) => ("not_run", Some("delivery_gate_not_wired"), Value::Null, 3),
208226
(_, Some(_)) => ("not_run", Some("action_not_wired"), Value::Null, 3),
209227
};
210-
println!(
211-
"{}",
228+
Ok((
212229
json!({
213230
"schema_version":"0.4.0", "report_type":"hook_execution_feedback",
214231
"plan":plan, "execution":execution, "reason":reason,
215232
"local_feedback":feedback, "delivery_decision":"not_evaluated",
216233
"host_blocking_verified":false, "soft_result_reused":false
217-
})
218-
);
219-
ExitCode::from(exit_code)
234+
}),
235+
exit_code,
236+
))
220237
}
221238

222239
fn task_verification_summary(

‎crates/codeguard-cli/src/lib.rs‎

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ pub mod checkstyle_probe_request;
8484
pub mod checkstyle_probe_result;
8585
mod checkstyle_task_recheck;
8686
mod checkstyle_workbench;
87+
#[cfg(unix)]
88+
pub mod claude_hook_command;
8789
pub mod config_command;
8890
pub mod conversation_feedback;
8991
pub mod corpus;
@@ -93,25 +95,11 @@ pub mod false_positive_decision;
9395
pub mod git_index_safety;
9496
#[cfg(unix)]
9597
pub mod git_index_safety_command;
96-
pub mod hook_plan_command;
97-
#[cfg(unix)]
98-
pub mod hook_execute_command;
99-
#[cfg(feature = "wasm-precheck")]
100-
pub mod syntax_worker_command;
101-
#[cfg(feature = "wasm-precheck")]
102-
pub mod syntax_worker_runner;
103-
#[cfg(feature = "wasm-precheck")]
104-
mod syntax_worker_envelope;
105-
#[cfg(feature = "wasm-precheck")]
106-
mod syntax_worker_recovery;
107-
#[cfg(feature = "wasm-precheck")]
108-
mod syntax_worker_candidate_observation;
109-
#[cfg(feature = "wasm-precheck")]
110-
mod typescript_syntax_precheck;
111-
#[cfg(feature = "wasm-precheck")]
112-
mod java_syntax_precheck;
11398
#[cfg(unix)]
11499
pub mod go_lint_command;
100+
#[cfg(unix)]
101+
pub mod hook_execute_command;
102+
pub mod hook_plan_command;
115103
pub mod init_command;
116104
mod java_checker_config_status;
117105
pub mod java_checkstyle_command;
@@ -128,6 +116,8 @@ pub mod java_lint_dispatch;
128116
#[cfg(unix)]
129117
pub mod java_p3c_command;
130118
mod java_p3c_scan;
119+
#[cfg(feature = "wasm-precheck")]
120+
mod java_syntax_precheck;
131121
pub mod legacy_v1_protocol;
132122
#[cfg(unix)]
133123
mod maven_dependency_probe;
@@ -174,13 +164,25 @@ pub mod rustdoc_repair_brief;
174164
#[cfg(unix)]
175165
pub(crate) mod rustdoc_task_recheck;
176166
pub mod sarif_feedback;
167+
#[cfg(feature = "wasm-precheck")]
168+
mod syntax_worker_candidate_observation;
169+
#[cfg(feature = "wasm-precheck")]
170+
pub mod syntax_worker_command;
171+
#[cfg(feature = "wasm-precheck")]
172+
mod syntax_worker_envelope;
173+
#[cfg(feature = "wasm-precheck")]
174+
mod syntax_worker_recovery;
175+
#[cfg(feature = "wasm-precheck")]
176+
pub mod syntax_worker_runner;
177177
#[cfg(unix)]
178178
pub mod task_attempt_command;
179179
#[cfg(unix)]
180180
pub mod task_lease_command;
181181
pub mod task_verify_command;
182182
pub mod tool_identity;
183183
pub mod tool_lock;
184+
#[cfg(feature = "wasm-precheck")]
185+
mod typescript_syntax_precheck;
184186
pub mod whitelist_command;
185187
mod whitelist_correction_command;
186188
mod whitelist_correction_projection;

‎crates/codeguard-cli/src/main.rs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ fn main() -> ExitCode {
3232
println!(
3333
"局部事件执行:hook execute PATH --timeout DURATION --format=json [--ruff-tool ABS_PATH] [--git-tool ABS_PATH] 从 stdin 读取同一事件协议;启动只发现、Stop 有界只读提示、repair_ready 复用 task verify 原工具复检、确认 Python 编辑调用 Ruff 局部快检、显式 Git 工具的 pre_commit 观察真实 index;完整交付始终 not_evaluated。"
3434
);
35+
println!(
36+
"Claude 候选软 Hook:hook claude post-tool-use PATH --timeout DURATION --format=json [--ruff-tool ABS_PATH] 从 stdin 读取成功文件工具事件,规范化项目内路径后复用 Rust 局部执行器;输出有界 additionalContext,宿主退出 0 不表示检查或交付通过。插件尚未自动接线。"
37+
);
3538
println!(
3639
"check all 的 Python CVE 节点可用 --pip-audit-tool ABS_PATH --pip-audit-version VERSION;逐构建根反馈原生结果或锁文件/工具阻塞,不签发零漏洞结论。"
3740
);
@@ -83,6 +86,10 @@ fn main() -> ExitCode {
8386
[command, operation, rest @ ..] if command == "hook" && operation == "execute" => {
8487
codeguard_cli::hook_execute_command::run(rest)
8588
}
89+
#[cfg(unix)]
90+
[command, operation, rest @ ..] if command == "hook" && operation == "claude" => {
91+
codeguard_cli::claude_hook_command::run(rest)
92+
}
8693
#[cfg(feature = "wasm-precheck")]
8794
[command, rest @ ..] if command == "__syntax-worker" => {
8895
codeguard_cli::syntax_worker_command::run(rest)

0 commit comments

Comments
 (0)