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).expect("serde_json::Value is always serializable");
54
55 w.write_all(json.as_bytes())
56 .map_err(|e| format!("write error: {e}"))?;
57
58 Ok(())
59}
60
61#[cfg(test)]
66mod tests {
67 use super::*;
68 use std::io::Cursor;
69
70 const CLAUDE_PRE_TOOL: &str = r#"{"type":"PreToolUse","tool_name":"Bash","tool_input":{"command":"ls -la"},"session_id":"sess_test_001"}"#;
71 const CURSOR_BEFORE_TOOL: &str = r#"{"type":"BeforeToolCall","toolCall":{"name":"run_terminal_cmd","args":{"command":"echo hi"}},"sessionId":"sess_test_002"}"#;
72
73 #[test]
74 fn read_from_parses_claude_code_event() {
75 let mut cursor = Cursor::new(CLAUDE_PRE_TOOL.as_bytes());
76 let event = read_from(&mut cursor).expect("read_from should succeed");
77 assert_eq!(event.caller, CallerKind::ClaudeCode);
78 assert_eq!(event.event.to_string(), "tool:before");
79 assert_eq!(event.tool.as_deref(), Some("bash"));
80 assert_eq!(event.session_id, "sess_test_001");
81 }
82
83 #[test]
84 fn read_from_returns_error_on_invalid_json() {
85 let mut cursor = Cursor::new(b"not valid json" as &[u8]);
86 let result = read_from(&mut cursor);
87 assert!(result.is_err());
88 let msg = result.unwrap_err();
89 assert!(msg.contains("JSON parse error") || msg.contains("parse"));
90 }
91
92 #[test]
93 fn respond_to_writes_json_to_writer() {
94 let mut cursor = Cursor::new(CLAUDE_PRE_TOOL.as_bytes());
96 let _ = read_from(&mut cursor).expect("read_from should succeed");
97
98 let mut output: Vec<u8> = Vec::new();
99 respond_to(&mut output, &HookResponse::approve()).expect("respond_to should succeed");
100
101 let json: serde_json::Value =
102 serde_json::from_slice(&output).expect("output should be valid JSON");
103 assert!(json.as_object().unwrap().is_empty());
105 }
106
107 #[test]
108 fn respond_to_block_uses_detected_caller() {
109 let mut cursor = Cursor::new(CURSOR_BEFORE_TOOL.as_bytes());
111 let _ = read_from(&mut cursor).expect("read_from should succeed");
112
113 let mut output: Vec<u8> = Vec::new();
114 respond_to(&mut output, &HookResponse::block("stop")).expect("respond_to should succeed");
115
116 let json: serde_json::Value =
117 serde_json::from_slice(&output).expect("output should be valid JSON");
118 assert_eq!(json["action"], "deny");
120 assert_eq!(json["message"], "stop");
121 }
122
123 #[test]
124 fn respond_to_modify_uses_detected_caller() {
125 let mut cursor = Cursor::new(CLAUDE_PRE_TOOL.as_bytes());
127 let _ = read_from(&mut cursor).expect("read_from should succeed");
128
129 let new_input = serde_json::json!({"command": "echo safe"});
130 let mut output: Vec<u8> = Vec::new();
131 respond_to(&mut output, &HookResponse::modify(new_input.clone()))
132 .expect("respond_to should succeed");
133
134 let json: serde_json::Value =
135 serde_json::from_slice(&output).expect("output should be valid JSON");
136 assert_eq!(json["decision"], "approve");
138 assert_eq!(json["tool_input"], new_input);
139 }
140
141 #[test]
142 fn last_caller_thread_local_is_updated_by_read_from() {
143 let mut cursor = Cursor::new(CLAUDE_PRE_TOOL.as_bytes());
145 let event = read_from(&mut cursor).expect("read_from should succeed");
146 assert_eq!(event.caller, CallerKind::ClaudeCode);
147
148 let mut output: Vec<u8> = Vec::new();
150 respond_to(&mut output, &HookResponse::approve()).expect("respond_to should succeed");
151 let json: serde_json::Value = serde_json::from_slice(&output).unwrap();
152 assert!(json.as_object().unwrap().is_empty());
153
154 let mut cursor2 = Cursor::new(CURSOR_BEFORE_TOOL.as_bytes());
156 let event2 = read_from(&mut cursor2).expect("read_from should succeed");
157 assert_eq!(event2.caller, CallerKind::Cursor);
158
159 let mut output2: Vec<u8> = Vec::new();
160 respond_to(&mut output2, &HookResponse::approve()).expect("respond_to should succeed");
161 let json2: serde_json::Value = serde_json::from_slice(&output2).unwrap();
162 assert_eq!(json2["action"], "allow");
164 }
165
166 #[test]
168 fn respond_delegates_to_stdout() {
169 let mut cursor = Cursor::new(CLAUDE_PRE_TOOL.as_bytes());
171 let _ = read_from(&mut cursor).expect("prime");
172 let result = respond(&HookResponse::approve());
174 assert!(result.is_ok());
175 }
176
177 #[cfg(unix)]
179 #[test]
180 fn read_delegates_to_stdin() {
181 extern "C" {
182 fn pipe(fds: *mut i32) -> i32;
183 fn dup(fd: i32) -> i32;
184 fn dup2(oldfd: i32, newfd: i32) -> i32;
185 fn close(fd: i32) -> i32;
186 fn write(fd: i32, buf: *const u8, count: usize) -> isize;
187 }
188
189 let json = CLAUDE_PRE_TOOL.as_bytes();
190
191 unsafe {
192 let mut fds = [0i32; 2];
193 assert_eq!(pipe(fds.as_mut_ptr()), 0);
194 let (read_fd, write_fd) = (fds[0], fds[1]);
195
196 write(write_fd, json.as_ptr(), json.len());
197 close(write_fd);
198
199 let saved = dup(0);
200 dup2(read_fd, 0);
201 close(read_fd);
202
203 let result = read();
204
205 dup2(saved, 0);
206 close(saved);
207
208 result.expect("read() should succeed");
209 }
210 }
211
212 #[test]
214 fn read_from_io_error_returns_err() {
215 struct FailReader;
216 impl std::io::Read for FailReader {
217 fn read(&mut self, _: &mut [u8]) -> std::io::Result<usize> {
218 Err(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "pipe broken"))
219 }
220 }
221 let result = read_from(&mut FailReader);
222 assert!(result.is_err());
223 assert!(result.unwrap_err().contains("read error"));
224 }
225
226 #[test]
228 fn respond_to_write_error_returns_err() {
229 struct FailWriter;
230 impl std::io::Write for FailWriter {
231 fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
232 Err(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "pipe broken"))
233 }
234 fn flush(&mut self) -> std::io::Result<()> { Ok(()) }
235 }
236 assert!(FailWriter.flush().is_ok());
237 let mut cursor = Cursor::new(CLAUDE_PRE_TOOL.as_bytes());
238 let _ = read_from(&mut cursor).expect("prime");
239 let result = respond_to(&mut FailWriter, &HookResponse::approve());
240 assert!(result.is_err());
241 assert!(result.unwrap_err().contains("write error"));
242 }
243}