Skip to main content

rusty_hooks/hook/
mod.rs

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}