use anyhow::Result;
use serde_json::Value;
use super::AgentKind;
use crate::hooks::{SessionStartHookInput, StopHookInput, ToolUseHookInput};
pub trait HookProtocol {
fn agent(&self) -> AgentKind;
fn parse_tool_use(&self, raw: &Value) -> Result<ToolUseHookInput>;
fn parse_post_tool_use(&self, raw: &Value) -> Result<ToolUseHookInput> {
self.parse_tool_use(raw)
}
fn parse_session_start(&self, raw: &Value) -> Result<SessionStartHookInput> {
Ok(SessionStartHookInput {
session_id: json_str(raw, "session_id").to_string(),
transcript_path: json_str(raw, "transcript_path").to_string(),
cwd: json_str(raw, "cwd").to_string(),
permission_mode: raw
.get("permission_mode")
.and_then(|v| v.as_str())
.map(|m| super::resolve_permission_mode(self.agent(), m).to_string()),
hook_event_name: json_str_or(raw, "hook_event_name", "SessionStart").to_string(),
source: raw.get("source").and_then(|v| v.as_str()).map(String::from),
model: raw.get("model").and_then(|v| v.as_str()).map(String::from),
})
}
fn parse_stop(&self, raw: &Value) -> Result<StopHookInput> {
Ok(StopHookInput {
session_id: json_str(raw, "session_id").to_string(),
transcript_path: json_str(raw, "transcript_path").to_string(),
cwd: json_str(raw, "cwd").to_string(),
hook_event_name: json_str_or(raw, "hook_event_name", "Stop").to_string(),
})
}
fn format_allow(
&self,
reason: Option<&str>,
_context: Option<&str>,
updated_input: Option<Value>,
) -> Value {
let mut output = serde_json::json!({ "decision": "allow" });
if let Some(r) = reason {
output["reason"] = Value::String(r.to_string());
}
if let Some(ui) = updated_input {
output["updated_input"] = ui;
}
output
}
fn format_deny(&self, reason: &str, _context: Option<&str>) -> Value {
serde_json::json!({
"decision": "deny",
"reason": reason
})
}
fn format_ask(&self, _reason: Option<&str>, _context: Option<&str>) -> Value {
serde_json::json!({ "continue": true })
}
fn format_session_start(&self, context: Option<&str>) -> Value {
let mut output = serde_json::json!({ "decision": "allow" });
if let Some(ctx) = context {
output["additional_context"] = Value::String(ctx.to_string());
}
output
}
fn rewrite_for_sandbox(&self, input: &ToolUseHookInput, sandbox_cmd: &str) -> Option<Value> {
if input.tool_name != "Bash" {
return None;
}
let command = input.tool_input.get("command")?.as_str()?;
let sandboxed = format!(
"{} shell --cwd {} -c {}",
shell_escape(sandbox_cmd),
shell_escape(&input.cwd),
shell_escape(command),
);
let mut updated = input.tool_input.clone();
updated
.as_object_mut()?
.insert("command".into(), Value::String(sandboxed));
Some(updated)
}
fn session_context(&self) -> &str {
"Clash is active and enforcing policy on this session.\n\
Run `clash commands` to see the full command hierarchy for managing policies, sandboxes, and debugging."
}
}
pub(crate) fn json_str<'a>(raw: &'a Value, field: &str) -> &'a str {
raw.get(field).and_then(|v| v.as_str()).unwrap_or("")
}
pub(crate) fn json_str_or<'a>(raw: &'a Value, field: &str, default: &'a str) -> &'a str {
raw.get(field).and_then(|v| v.as_str()).unwrap_or(default)
}
pub(crate) fn json_str_any<'a>(raw: &'a Value, fields: &[&str]) -> &'a str {
for field in fields {
if let Some(s) = raw.get(*field).and_then(|v| v.as_str()) {
return s;
}
}
""
}
pub(crate) fn json_value_any(raw: &Value, fields: &[&str]) -> Option<Value> {
for field in fields {
if let Some(v) = raw.get(*field) {
return Some(v.clone());
}
}
None
}
pub(crate) fn shell_escape(s: &str) -> String {
format!("'{}'", s.replace('\'', "'\\''"))
}
pub fn get_protocol(agent: AgentKind) -> Box<dyn HookProtocol> {
match agent {
AgentKind::Claude => Box::new(super::claude::ClaudeProtocol),
AgentKind::Gemini => Box::new(super::gemini::GeminiProtocol),
AgentKind::Codex => Box::new(super::codex::CodexProtocol),
AgentKind::AmazonQ => Box::new(super::amazonq::AmazonQProtocol),
AgentKind::OpenCode => Box::new(super::opencode::OpenCodeProtocol),
AgentKind::Copilot => Box::new(super::copilot::CopilotProtocol),
}
}