1pub mod fs;
2mod payload;
3mod validate;
4
5pub use payload::Payload;
6
7use std::io::{self, Read};
8
9pub fn block(reason: &str) -> ! {
10 let msg = serde_json::json!({ "decision": "block", "reason": reason });
11 println!("{msg}");
12 std::process::exit(2);
13}
14
15pub fn run() -> Result<(), String> {
16 let payload = read_payload()?;
17
18 let tool_name = match payload.tool_name() {
19 Some(name @ ("Edit" | "Write")) => name,
20 _ => return Ok(()),
21 };
22
23 let tool_input = payload
24 .tool_input()
25 .ok_or("Hook payload missing tool_input object")?;
26
27 let file_path = tool_input
28 .file_path()
29 .ok_or("Hook payload missing tool_input.file_path")?;
30
31 let proj = fs::project_root(payload.cwd());
32 let target = fs::resolve_file(&proj, file_path)
33 .ok_or_else(|| format!("Path traversal detected: {file_path}"))?;
34
35 if !target.is_file() {
36 return Err(format!("Edited file does not exist: {}", target.display()));
37 }
38
39 let content = fs::read_text(&target)
40 .ok_or_else(|| format!("Failed to read file: {}", target.display()))?;
41
42 match tool_name {
43 "Write" => validate::verify_write(tool_input, &content, &target)?,
44 _ => validate::verify_edit(tool_input, &content, &target)?,
45 }
46
47 Ok(())
48}
49
50fn read_payload() -> Result<Payload, String> {
51 let mut raw = String::new();
52 io::stdin()
53 .read_to_string(&mut raw)
54 .map_err(|e| format!("Failed to read stdin: {e}"))?;
55
56 serde_json::from_str(&raw).map_err(|e| format!("Invalid JSON input: {e}"))
57}