Skip to main content

polyhook_core/
response.rs

1use crate::types::{CallerKind, HookResponse};
2use serde_json::{json, Value};
3
4/// Serialize a [`HookResponse`] into the JSON format expected by the detected caller.
5pub fn serialize_response(resp: &HookResponse, caller: &CallerKind) -> Value {
6    match caller {
7        CallerKind::ClaudeCode | CallerKind::Unknown => serialize_claude_code(resp),
8        CallerKind::Cursor => serialize_cursor(resp),
9        CallerKind::Windsurf => serialize_windsurf(resp),
10        CallerKind::Cline => serialize_cline(resp),
11        CallerKind::Amp => serialize_amp(resp),
12    }
13}
14
15// ---------------------------------------------------------------------------
16// Per-caller serializers
17// ---------------------------------------------------------------------------
18
19fn serialize_claude_code(resp: &HookResponse) -> Value {
20    match resp {
21        HookResponse::ApproveResponse(_) => json!({}),
22        HookResponse::BlockResponse(b) => {
23            json!({ "decision": "block", "reason": b.message })
24        }
25        HookResponse::ModifyResponse(m) => {
26            json!({ "decision": "approve", "tool_input": m.input })
27        }
28    }
29}
30
31fn serialize_cursor(resp: &HookResponse) -> Value {
32    match resp {
33        HookResponse::ApproveResponse(_) => json!({ "action": "allow" }),
34        HookResponse::BlockResponse(b) => {
35            json!({ "action": "deny", "message": b.message })
36        }
37        HookResponse::ModifyResponse(m) => {
38            json!({ "action": "modify", "args": m.input })
39        }
40    }
41}
42
43fn serialize_windsurf(resp: &HookResponse) -> Value {
44    match resp {
45        HookResponse::ApproveResponse(_) => json!({ "allow": true }),
46        HookResponse::BlockResponse(b) => {
47            json!({ "allow": false, "reason": b.message })
48        }
49        HookResponse::ModifyResponse(m) => {
50            json!({ "allow": true, "modified_parameters": m.input })
51        }
52    }
53}
54
55fn serialize_cline(resp: &HookResponse) -> Value {
56    match resp {
57        HookResponse::ApproveResponse(_) => json!({ "approved": true }),
58        HookResponse::BlockResponse(b) => {
59            json!({ "approved": false, "reason": b.message })
60        }
61        HookResponse::ModifyResponse(m) => {
62            json!({ "approved": true, "modifiedInput": m.input })
63        }
64    }
65}
66
67fn serialize_amp(resp: &HookResponse) -> Value {
68    match resp {
69        HookResponse::ApproveResponse(_) => json!({ "result": "allow" }),
70        HookResponse::BlockResponse(b) => {
71            json!({ "result": "deny", "reason": b.message })
72        }
73        HookResponse::ModifyResponse(m) => {
74            json!({ "result": "allow", "modified": m.input })
75        }
76    }
77}
78
79// ---------------------------------------------------------------------------
80// Tests
81// ---------------------------------------------------------------------------
82
83#[cfg(test)]
84mod tests {
85    use super::serialize_response;
86    use crate::types::{CallerKind, HookResponse};
87    use serde_json::json;
88
89    #[test]
90    fn claude_code_approve() {
91        let val = serialize_response(&HookResponse::approve(), &CallerKind::ClaudeCode);
92        assert_eq!(val, json!({}));
93    }
94
95    #[test]
96    fn claude_code_block() {
97        let val = serialize_response(&HookResponse::block("not allowed"), &CallerKind::ClaudeCode);
98        assert_eq!(val["decision"], json!("block"));
99        assert_eq!(val["reason"], json!("not allowed"));
100    }
101
102    #[test]
103    fn claude_code_modify() {
104        let new_input = json!({"command": "echo safe"});
105        let val = serialize_response(&HookResponse::modify(new_input.clone()), &CallerKind::ClaudeCode);
106        assert_eq!(val["decision"], json!("approve"));
107        assert_eq!(val["tool_input"], new_input);
108    }
109
110    #[test]
111    fn unknown_approve() {
112        let val = serialize_response(&HookResponse::approve(), &CallerKind::Unknown);
113        assert_eq!(val, json!({}));
114    }
115
116    #[test]
117    fn unknown_block() {
118        let val = serialize_response(&HookResponse::block("denied"), &CallerKind::Unknown);
119        assert_eq!(val["decision"], json!("block"));
120        assert_eq!(val["reason"], json!("denied"));
121    }
122
123    #[test]
124    fn unknown_modify() {
125        let new_input = json!({"x": 1});
126        let val = serialize_response(&HookResponse::modify(new_input.clone()), &CallerKind::Unknown);
127        assert_eq!(val["decision"], json!("approve"));
128        assert_eq!(val["tool_input"], new_input);
129    }
130
131    #[test]
132    fn cursor_approve() {
133        let val = serialize_response(&HookResponse::approve(), &CallerKind::Cursor);
134        assert_eq!(val["action"], json!("allow"));
135    }
136
137    #[test]
138    fn cursor_block() {
139        let val = serialize_response(&HookResponse::block("too risky"), &CallerKind::Cursor);
140        assert_eq!(val["action"], json!("deny"));
141        assert_eq!(val["message"], json!("too risky"));
142    }
143
144    #[test]
145    fn cursor_modify() {
146        let new_input = json!({"command": "echo safe"});
147        let val = serialize_response(&HookResponse::modify(new_input.clone()), &CallerKind::Cursor);
148        assert_eq!(val["action"], json!("modify"));
149        assert_eq!(val["args"], new_input);
150    }
151
152    #[test]
153    fn windsurf_approve() {
154        let val = serialize_response(&HookResponse::approve(), &CallerKind::Windsurf);
155        assert_eq!(val["allow"], json!(true));
156    }
157
158    #[test]
159    fn windsurf_block() {
160        let val = serialize_response(&HookResponse::block("blocked by policy"), &CallerKind::Windsurf);
161        assert_eq!(val["allow"], json!(false));
162        assert_eq!(val["reason"], json!("blocked by policy"));
163    }
164
165    #[test]
166    fn windsurf_modify() {
167        let new_input = json!({"path": "/safe/dir"});
168        let val = serialize_response(&HookResponse::modify(new_input.clone()), &CallerKind::Windsurf);
169        assert_eq!(val["allow"], json!(true));
170        assert_eq!(val["modified_parameters"], new_input);
171    }
172
173    #[test]
174    fn cline_approve() {
175        let val = serialize_response(&HookResponse::approve(), &CallerKind::Cline);
176        assert_eq!(val["approved"], json!(true));
177    }
178
179    #[test]
180    fn cline_block() {
181        let val = serialize_response(&HookResponse::block("cline blocked"), &CallerKind::Cline);
182        assert_eq!(val["approved"], json!(false));
183        assert_eq!(val["reason"], json!("cline blocked"));
184    }
185
186    #[test]
187    fn cline_modify() {
188        let new_input = json!({"command": "ls /safe"});
189        let val = serialize_response(&HookResponse::modify(new_input.clone()), &CallerKind::Cline);
190        assert_eq!(val["approved"], json!(true));
191        assert_eq!(val["modifiedInput"], new_input);
192    }
193
194    #[test]
195    fn amp_approve() {
196        let val = serialize_response(&HookResponse::approve(), &CallerKind::Amp);
197        assert_eq!(val["result"], json!("allow"));
198    }
199
200    #[test]
201    fn amp_block() {
202        let val = serialize_response(&HookResponse::block("amp denied"), &CallerKind::Amp);
203        assert_eq!(val["result"], json!("deny"));
204        assert_eq!(val["reason"], json!("amp denied"));
205    }
206
207    #[test]
208    fn amp_modify() {
209        let new_input = json!({"command": "echo ok"});
210        let val = serialize_response(&HookResponse::modify(new_input.clone()), &CallerKind::Amp);
211        assert_eq!(val["result"], json!("allow"));
212        assert_eq!(val["modified"], new_input);
213    }
214}