polyhook_core/
response.rs1use crate::types::{CallerKind, HookEventEvent, HookResponse};
2use serde_json::{json, Value};
3
4pub fn serialize_response(resp: &HookResponse, caller: &CallerKind) -> Value {
9 serialize_response_with_event(resp, *caller, None)
10}
11
12pub(crate) fn serialize_response_with_event(
16 resp: &HookResponse,
17 caller: CallerKind,
18 event: Option<HookEventEvent>,
19) -> Value {
20 if let HookResponse::ContextResponse(c) = resp {
21 if let Some(value) = serialize_context(&c.context, caller) {
22 return value;
23 }
24 }
25 match caller {
26 CallerKind::ClaudeCode | CallerKind::Pi | CallerKind::Codex | CallerKind::Unknown => {
27 serialize_claude_code(resp, event)
28 }
29 CallerKind::Cursor => serialize_cursor(resp),
30 CallerKind::Windsurf => serialize_windsurf(resp),
31 CallerKind::Cline => serialize_cline(resp),
32 CallerKind::Amp => serialize_amp(resp),
33 CallerKind::GeminiCli => serialize_gemini_cli(resp),
34 CallerKind::Hermes => serialize_hermes(resp),
35 }
36}
37
38fn serialize_context(context: &str, caller: CallerKind) -> Option<Value> {
42 match caller {
43 CallerKind::ClaudeCode | CallerKind::Pi | CallerKind::Codex | CallerKind::Unknown => {
44 Some(json!({
45 "hookSpecificOutput": {
46 "hookEventName": "UserPromptSubmit",
47 "additionalContext": context
48 }
49 }))
50 }
51 CallerKind::GeminiCli => Some(json!({
52 "hookSpecificOutput": {
53 "hookEventName": "BeforeAgent",
54 "additionalContext": context
55 }
56 })),
57 CallerKind::Hermes => Some(json!({ "context": context })),
58 CallerKind::Cline => Some(json!({ "cancel": false, "contextModification": context })),
59 CallerKind::Cursor | CallerKind::Windsurf | CallerKind::Amp => None,
60 }
61}
62
63fn serialize_claude_code(resp: &HookResponse, event: Option<HookEventEvent>) -> Value {
68 let is_pre_tool_use = matches!(event, Some(HookEventEvent::ToolBefore));
69 match resp {
70 HookResponse::ApproveResponse(_) | HookResponse::ContextResponse(_) => {
71 if is_pre_tool_use {
72 json!({
76 "hookSpecificOutput": {
77 "hookEventName": "PreToolUse",
78 "permissionDecision": "allow"
79 }
80 })
81 } else {
82 json!({})
83 }
84 }
85 HookResponse::BlockResponse(b) => {
86 if is_pre_tool_use {
87 json!({
88 "hookSpecificOutput": {
89 "hookEventName": "PreToolUse",
90 "permissionDecision": "deny",
91 "permissionDecisionReason": b.message
92 }
93 })
94 } else {
95 json!({ "decision": "block", "reason": b.message })
96 }
97 }
98 HookResponse::ModifyResponse(m) => {
99 if is_pre_tool_use {
100 json!({
104 "hookSpecificOutput": {
105 "hookEventName": "PreToolUse",
106 "permissionDecision": "allow",
107 "updatedInput": m.input
108 }
109 })
110 } else {
111 json!({ "decision": "approve", "tool_input": m.input })
112 }
113 }
114 }
115}
116
117fn serialize_cursor(resp: &HookResponse) -> Value {
118 match resp {
119 HookResponse::ApproveResponse(_) | HookResponse::ContextResponse(_) => {
120 json!({ "action": "allow" })
121 }
122 HookResponse::BlockResponse(b) => {
123 json!({ "action": "deny", "message": b.message })
124 }
125 HookResponse::ModifyResponse(m) => {
126 json!({ "action": "modify", "args": m.input })
127 }
128 }
129}
130
131fn serialize_windsurf(resp: &HookResponse) -> Value {
132 match resp {
133 HookResponse::ApproveResponse(_) | HookResponse::ContextResponse(_) => {
134 json!({ "allow": true })
135 }
136 HookResponse::BlockResponse(b) => {
137 json!({ "allow": false, "reason": b.message })
138 }
139 HookResponse::ModifyResponse(m) => {
140 json!({ "allow": true, "modified_parameters": m.input })
141 }
142 }
143}
144
145fn serialize_cline(resp: &HookResponse) -> Value {
146 match resp {
147 HookResponse::ApproveResponse(_) | HookResponse::ContextResponse(_) => {
148 json!({ "approved": true })
149 }
150 HookResponse::BlockResponse(b) => {
151 json!({ "approved": false, "reason": b.message })
152 }
153 HookResponse::ModifyResponse(m) => {
154 json!({ "approved": true, "modifiedInput": m.input })
155 }
156 }
157}
158
159fn serialize_amp(resp: &HookResponse) -> Value {
160 match resp {
161 HookResponse::ApproveResponse(_) | HookResponse::ContextResponse(_) => {
162 json!({ "result": "allow" })
163 }
164 HookResponse::BlockResponse(b) => {
165 json!({ "result": "deny", "reason": b.message })
166 }
167 HookResponse::ModifyResponse(m) => {
168 json!({ "result": "allow", "modified": m.input })
169 }
170 }
171}
172
173fn serialize_gemini_cli(resp: &HookResponse) -> Value {
174 match resp {
175 HookResponse::ApproveResponse(_) | HookResponse::ContextResponse(_) => {
176 json!({ "decision": "allow" })
177 }
178 HookResponse::BlockResponse(b) => {
179 json!({ "decision": "deny", "reason": b.message })
180 }
181 HookResponse::ModifyResponse(m) => {
182 json!({ "decision": "allow", "tool_input": m.input })
183 }
184 }
185}
186
187fn serialize_hermes(resp: &HookResponse) -> Value {
188 match resp {
189 HookResponse::ApproveResponse(_) | HookResponse::ContextResponse(_) => json!({}),
190 HookResponse::BlockResponse(b) => {
191 json!({ "action": "block", "message": b.message })
192 }
193 HookResponse::ModifyResponse(m) => {
194 json!({ "action": "modify", "tool_input": m.input })
195 }
196 }
197}
198
199#[cfg(test)]
204#[path = "response_tests.rs"]
205mod tests;