coding_agent_hooks/agents/
claude.rs1use anyhow::Result;
8use serde_json::Value;
9
10use super::{AgentKind, resolve_permission_mode, resolve_tool_name};
11use crate::input::{SessionStartHookInput, StopHookInput, ToolUseHookInput};
12use crate::output::HookOutput;
13use crate::protocol::HookProtocol;
14
15pub struct ClaudeProtocol;
16
17impl HookProtocol for ClaudeProtocol {
18 fn agent(&self) -> AgentKind {
19 AgentKind::Claude
20 }
21
22 fn parse_tool_use(&self, raw: &Value) -> Result<ToolUseHookInput> {
23 let mut input: ToolUseHookInput = serde_json::from_value(raw.clone())?;
24 let original = input.tool_name.clone();
25 input.tool_name = resolve_tool_name(AgentKind::Claude, &original).to_string();
26 input.original_tool_name = Some(original);
27 input.agent = Some(AgentKind::Claude);
28 input.permission_mode =
29 resolve_permission_mode(AgentKind::Claude, &input.permission_mode).to_string();
30 Ok(input)
31 }
32
33 fn parse_session_start(&self, raw: &Value) -> Result<SessionStartHookInput> {
34 let mut input: SessionStartHookInput = serde_json::from_value(raw.clone())?;
35 if let Some(mode) = &input.permission_mode {
36 input.permission_mode =
37 Some(resolve_permission_mode(AgentKind::Claude, mode).to_string());
38 }
39 Ok(input)
40 }
41
42 fn parse_stop(&self, raw: &Value) -> Result<StopHookInput> {
43 Ok(serde_json::from_value(raw.clone())?)
44 }
45
46 fn format_allow(
49 &self,
50 reason: Option<&str>,
51 context: Option<&str>,
52 updated_input: Option<Value>,
53 ) -> Value {
54 let mut output = HookOutput::allow(reason.map(String::from), context.map(String::from));
55 if let Some(ui) = updated_input {
56 output.set_updated_input(ui);
57 }
58 serde_json::to_value(output).expect("HookOutput serialization cannot fail")
59 }
60
61 fn format_deny(&self, reason: &str, context: Option<&str>) -> Value {
62 let output = HookOutput::deny(reason.to_string(), context.map(String::from));
63 serde_json::to_value(output).expect("HookOutput serialization cannot fail")
64 }
65
66 fn format_ask(&self, reason: Option<&str>, context: Option<&str>) -> Value {
67 let output = HookOutput::ask(reason.map(String::from), context.map(String::from));
68 serde_json::to_value(output).expect("HookOutput serialization cannot fail")
69 }
70
71 fn format_session_start(&self, context: Option<&str>) -> Value {
72 let output = HookOutput::session_start(context.map(String::from));
73 serde_json::to_value(output).expect("HookOutput serialization cannot fail")
74 }
75
76 }
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
85 fn parse_tool_use_normalizes_name() {
86 let raw = serde_json::json!({
87 "session_id": "test",
88 "transcript_path": "/tmp/t.jsonl",
89 "cwd": "/tmp",
90 "permission_mode": "default",
91 "hook_event_name": "PreToolUse",
92 "tool_name": "Bash",
93 "tool_input": {"command": "ls"},
94 "tool_use_id": "toolu_01"
95 });
96 let input = ClaudeProtocol.parse_tool_use(&raw).unwrap();
97 assert_eq!(input.tool_name, "Bash");
98 assert_eq!(input.original_tool_name.as_deref(), Some("Bash"));
99 assert_eq!(input.agent, Some(AgentKind::Claude));
100 }
101
102 #[test]
103 fn format_allow_matches_existing_format() {
104 let output = ClaudeProtocol.format_allow(Some("safe"), None, None);
105 assert_eq!(output["continue"], true);
106 assert_eq!(output["hookSpecificOutput"]["permissionDecision"], "allow");
107 }
108
109 #[test]
110 fn format_deny_matches_existing_format() {
111 let output = ClaudeProtocol.format_deny("blocked", Some("context"));
112 assert_eq!(output["continue"], true);
113 assert_eq!(output["hookSpecificOutput"]["permissionDecision"], "deny");
114 }
115
116 #[test]
117 fn format_ask_matches_existing_format() {
118 let output = ClaudeProtocol.format_ask(None, None);
119 assert_eq!(output["continue"], true);
120 assert_eq!(output["hookSpecificOutput"]["permissionDecision"], "ask");
121 }
122
123 #[test]
124 fn rewrite_for_sandbox_uses_default() {
125 let input = ToolUseHookInput {
126 tool_name: "Bash".into(),
127 tool_input: serde_json::json!({"command": "ls -la"}),
128 cwd: "/home/user".into(),
129 ..Default::default()
130 };
131 let result = ClaudeProtocol
132 .rewrite_for_sandbox(&input, "/usr/bin/clash")
133 .unwrap();
134 assert!(result["command"].as_str().unwrap().contains("clash"));
135 }
136}