1pub mod detect;
2pub mod events;
3pub mod parse;
4pub mod response;
5pub mod tools;
6pub mod types;
7mod type_impls;
8pub mod wasm;
9mod stdin_wrappers;
10
11pub use types::*;
12pub use stdin_wrappers::{read, respond};
13
14use std::cell::RefCell;
15use std::io::{Read, Write};
16
17use parse::parse_event;
18use response::serialize_response;
19
20thread_local! {
24 static LAST_CALLER: RefCell<CallerKind> = RefCell::new(CallerKind::Unknown);
25}
26
27pub fn read_from(r: &mut impl Read) -> Result<HookEvent, String> {
33 let mut buf = Vec::new();
34 r.read_to_end(&mut buf)
35 .map_err(|e| format!("read error: {e}"))?;
36
37 let event = parse_event(&buf)?;
38
39 LAST_CALLER.with(|c| {
41 *c.borrow_mut() = event.caller.clone();
42 });
43
44 Ok(event)
45}
46
47pub fn respond_to(w: &mut impl Write, response: &HookResponse) -> Result<(), String> {
50 let caller = LAST_CALLER.with(|c| c.borrow().clone());
51 let value = serialize_response(response, &caller);
52 let json = serde_json::to_string(&value).map_err(|e| format!("JSON encode error: {e}"))?;
53
54 w.write_all(json.as_bytes())
55 .map_err(|e| format!("write error: {e}"))?;
56
57 Ok(())
58}
59
60#[cfg(test)]
65mod tests {
66 use super::*;
67 use std::io::Cursor;
68
69 const CLAUDE_PRE_TOOL: &str = r#"{"type":"PreToolUse","tool_name":"Bash","tool_input":{"command":"ls -la"},"session_id":"sess_test_001"}"#;
70 const CURSOR_BEFORE_TOOL: &str = r#"{"type":"BeforeToolCall","toolCall":{"name":"run_terminal_cmd","args":{"command":"echo hi"}},"sessionId":"sess_test_002"}"#;
71
72 #[test]
73 fn read_from_parses_claude_code_event() {
74 let mut cursor = Cursor::new(CLAUDE_PRE_TOOL.as_bytes());
75 let event = read_from(&mut cursor).expect("read_from should succeed");
76 assert_eq!(event.caller, CallerKind::ClaudeCode);
77 assert_eq!(event.event.to_string(), "tool:before");
78 assert_eq!(event.tool.as_deref(), Some("bash"));
79 assert_eq!(event.session_id, "sess_test_001");
80 }
81
82 #[test]
83 fn read_from_returns_error_on_invalid_json() {
84 let mut cursor = Cursor::new(b"not valid json" as &[u8]);
85 let result = read_from(&mut cursor);
86 assert!(result.is_err());
87 let msg = result.unwrap_err();
88 assert!(msg.contains("JSON parse error") || msg.contains("parse"));
89 }
90
91 #[test]
92 fn respond_to_writes_json_to_writer() {
93 let mut cursor = Cursor::new(CLAUDE_PRE_TOOL.as_bytes());
95 let _ = read_from(&mut cursor).expect("read_from should succeed");
96
97 let mut output: Vec<u8> = Vec::new();
98 respond_to(&mut output, &HookResponse::approve()).expect("respond_to should succeed");
99
100 let json: serde_json::Value =
101 serde_json::from_slice(&output).expect("output should be valid JSON");
102 assert!(json.as_object().unwrap().is_empty());
104 }
105
106 #[test]
107 fn respond_to_block_uses_detected_caller() {
108 let mut cursor = Cursor::new(CURSOR_BEFORE_TOOL.as_bytes());
110 let _ = read_from(&mut cursor).expect("read_from should succeed");
111
112 let mut output: Vec<u8> = Vec::new();
113 respond_to(&mut output, &HookResponse::block("stop")).expect("respond_to should succeed");
114
115 let json: serde_json::Value =
116 serde_json::from_slice(&output).expect("output should be valid JSON");
117 assert_eq!(json["action"], "deny");
119 assert_eq!(json["message"], "stop");
120 }
121
122 #[test]
123 fn respond_to_modify_uses_detected_caller() {
124 let mut cursor = Cursor::new(CLAUDE_PRE_TOOL.as_bytes());
126 let _ = read_from(&mut cursor).expect("read_from should succeed");
127
128 let new_input = serde_json::json!({"command": "echo safe"});
129 let mut output: Vec<u8> = Vec::new();
130 respond_to(&mut output, &HookResponse::modify(new_input.clone()))
131 .expect("respond_to should succeed");
132
133 let json: serde_json::Value =
134 serde_json::from_slice(&output).expect("output should be valid JSON");
135 assert_eq!(json["decision"], "approve");
137 assert_eq!(json["tool_input"], new_input);
138 }
139
140 #[test]
141 fn last_caller_thread_local_is_updated_by_read_from() {
142 let mut cursor = Cursor::new(CLAUDE_PRE_TOOL.as_bytes());
144 let event = read_from(&mut cursor).expect("read_from should succeed");
145 assert_eq!(event.caller, CallerKind::ClaudeCode);
146
147 let mut output: Vec<u8> = Vec::new();
149 respond_to(&mut output, &HookResponse::approve()).expect("respond_to should succeed");
150 let json: serde_json::Value = serde_json::from_slice(&output).unwrap();
151 assert!(json.as_object().unwrap().is_empty());
152
153 let mut cursor2 = Cursor::new(CURSOR_BEFORE_TOOL.as_bytes());
155 let event2 = read_from(&mut cursor2).expect("read_from should succeed");
156 assert_eq!(event2.caller, CallerKind::Cursor);
157
158 let mut output2: Vec<u8> = Vec::new();
159 respond_to(&mut output2, &HookResponse::approve()).expect("respond_to should succeed");
160 let json2: serde_json::Value = serde_json::from_slice(&output2).unwrap();
161 assert_eq!(json2["action"], "allow");
163 }
164}