coding_agent_hooks/
protocol.rs1use anyhow::Result;
14use serde_json::Value;
15
16use crate::agents::{AgentKind, resolve_permission_mode};
17use crate::input::{SessionStartHookInput, StopHookInput, ToolUseHookInput};
18
19pub trait HookProtocol {
33 fn agent(&self) -> AgentKind;
35
36 fn parse_tool_use(&self, raw: &Value) -> Result<ToolUseHookInput>;
42
43 fn parse_post_tool_use(&self, raw: &Value) -> Result<ToolUseHookInput> {
47 self.parse_tool_use(raw)
48 }
49
50 fn parse_session_start(&self, raw: &Value) -> Result<SessionStartHookInput> {
54 Ok(SessionStartHookInput {
55 session_id: json_str(raw, "session_id").to_string(),
56 transcript_path: json_str(raw, "transcript_path").to_string(),
57 cwd: json_str(raw, "cwd").to_string(),
58 permission_mode: raw
59 .get("permission_mode")
60 .and_then(|v| v.as_str())
61 .map(|m| resolve_permission_mode(self.agent(), m).to_string()),
62 hook_event_name: json_str_or(raw, "hook_event_name", "SessionStart").to_string(),
63 source: raw.get("source").and_then(|v| v.as_str()).map(String::from),
64 model: raw.get("model").and_then(|v| v.as_str()).map(String::from),
65 })
66 }
67
68 fn parse_stop(&self, raw: &Value) -> Result<StopHookInput> {
72 Ok(StopHookInput {
73 session_id: json_str(raw, "session_id").to_string(),
74 transcript_path: json_str(raw, "transcript_path").to_string(),
75 cwd: json_str(raw, "cwd").to_string(),
76 hook_event_name: json_str_or(raw, "hook_event_name", "Stop").to_string(),
77 })
78 }
79
80 fn format_allow(
84 &self,
85 reason: Option<&str>,
86 _context: Option<&str>,
87 updated_input: Option<Value>,
88 ) -> Value {
89 let mut output = serde_json::json!({ "decision": "allow" });
90 if let Some(r) = reason {
91 output["reason"] = Value::String(r.to_string());
92 }
93 if let Some(ui) = updated_input {
94 output["updated_input"] = ui;
95 }
96 output
97 }
98
99 fn format_deny(&self, reason: &str, _context: Option<&str>) -> Value {
103 serde_json::json!({
104 "decision": "deny",
105 "reason": reason
106 })
107 }
108
109 fn format_ask(&self, _reason: Option<&str>, _context: Option<&str>) -> Value {
113 serde_json::json!({ "continue": true })
114 }
115
116 fn format_session_start(&self, context: Option<&str>) -> Value {
120 let mut output = serde_json::json!({ "decision": "allow" });
121 if let Some(ctx) = context {
122 output["additional_context"] = Value::String(ctx.to_string());
123 }
124 output
125 }
126
127 fn rewrite_for_sandbox(&self, input: &ToolUseHookInput, sandbox_cmd: &str) -> Option<Value> {
131 if input.tool_name != "Bash" {
132 return None;
133 }
134 let command = input.tool_input.get("command")?.as_str()?;
135 let sandboxed = format!(
136 "{} shell --cwd {} -c {}",
137 shell_escape(sandbox_cmd),
138 shell_escape(&input.cwd),
139 shell_escape(command),
140 );
141 let mut updated = input.tool_input.clone();
142 updated
143 .as_object_mut()?
144 .insert("command".into(), Value::String(sandboxed));
145 Some(updated)
146 }
147
148 fn session_context(&self) -> &str {
152 "Agent hooks are active and enforcing policy on this session."
153 }
154}
155
156pub fn json_str<'a>(raw: &'a Value, field: &str) -> &'a str {
162 raw.get(field).and_then(|v| v.as_str()).unwrap_or("")
163}
164
165pub fn json_str_or<'a>(raw: &'a Value, field: &str, default: &'a str) -> &'a str {
167 raw.get(field).and_then(|v| v.as_str()).unwrap_or(default)
168}
169
170pub fn json_str_any<'a>(raw: &'a Value, fields: &[&str]) -> &'a str {
172 for field in fields {
173 if let Some(s) = raw.get(*field).and_then(|v| v.as_str()) {
174 return s;
175 }
176 }
177 ""
178}
179
180pub fn json_value_any(raw: &Value, fields: &[&str]) -> Option<Value> {
182 for field in fields {
183 if let Some(v) = raw.get(*field) {
184 return Some(v.clone());
185 }
186 }
187 None
188}
189
190pub fn shell_escape(s: &str) -> String {
192 format!("'{}'", s.replace('\'', "'\\''"))
193}
194
195pub fn get_protocol(agent: AgentKind) -> Box<dyn HookProtocol> {
197 match agent {
198 AgentKind::Claude => Box::new(crate::agents::claude::ClaudeProtocol),
199 AgentKind::Gemini => Box::new(crate::agents::gemini::GeminiProtocol),
200 AgentKind::Codex => Box::new(crate::agents::codex::CodexProtocol),
201 AgentKind::AmazonQ => Box::new(crate::agents::amazonq::AmazonQProtocol),
202 AgentKind::OpenCode => Box::new(crate::agents::opencode::OpenCodeProtocol),
203 AgentKind::Copilot => Box::new(crate::agents::copilot::CopilotProtocol),
204 }
205}