1use serde::Deserialize;
2use std::path::PathBuf;
3
4#[derive(Debug, Clone, Deserialize)]
7pub struct HookContext {
8 pub session_id: String,
9 pub transcript_path: PathBuf,
10 pub cwd: PathBuf,
11 pub hook_event_name: HookEvent,
12 #[serde(default)]
16 pub tool_name: String,
17 #[serde(default)]
18 pub tool_use_id: String,
19 #[serde(default)]
20 pub permission_mode: Option<String>,
21 #[serde(default)]
22 pub effort: Option<EffortLevel>,
23 #[serde(default)]
25 pub agent_id: Option<String>,
26 #[serde(default)]
28 pub agent_type: Option<String>,
29}
30
31impl HookContext {
32 pub fn agent_key(&self) -> &str {
34 self.agent_id.as_deref().unwrap_or("main")
35 }
36}
37
38#[derive(Debug, Copy, Clone, Deserialize, PartialEq, Eq)]
39pub enum HookEvent {
40 #[serde(rename = "PreToolUse")]
41 Pre,
42 #[serde(rename = "PostToolUse")]
43 Post,
44 #[serde(rename = "PostToolUseFailure")]
48 PostFailure,
49 #[serde(rename = "SubagentStop")]
54 SubagentStop,
55}
56
57#[derive(Debug, Clone, Deserialize)]
58pub struct EffortLevel {
59 pub level: String,
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65
66 const MAIN_AGENT_PAYLOAD: &str = r#"{
68 "session_id": "a9db5455-5a02-44b1-b807-0bf79d80e6b1",
69 "transcript_path": "/Users/brandon/.claude/projects/-private-tmp-ccprobe/a9db5455-5a02-44b1-b807-0bf79d80e6b1.jsonl",
70 "cwd": "/private/tmp/ccprobe",
71 "hook_event_name": "PreToolUse",
72 "tool_name": "Bash",
73 "tool_use_id": "toolu_01",
74 "permission_mode": "bypassPermissions",
75 "effort": {"level": "medium"}
76 }"#;
77
78 const SUBAGENT_PAYLOAD: &str = r#"{
79 "session_id": "a9db5455-5a02-44b1-b807-0bf79d80e6b1",
80 "transcript_path": "/Users/brandon/.claude/projects/-private-tmp-ccprobe/a9db5455-5a02-44b1-b807-0bf79d80e6b1.jsonl",
81 "cwd": "/private/tmp/ccprobe",
82 "hook_event_name": "PreToolUse",
83 "tool_name": "Bash",
84 "tool_use_id": "toolu_02",
85 "agent_id": "a56e70ccdc442bf74",
86 "agent_type": "general-purpose"
87 }"#;
88
89 #[test]
90 fn deserializes_main_agent_payload() {
91 let ctx: HookContext = serde_json::from_str(MAIN_AGENT_PAYLOAD).unwrap();
92 assert_eq!(ctx.tool_name, "Bash");
93 assert_eq!(ctx.hook_event_name, HookEvent::Pre);
94 assert!(ctx.agent_id.is_none());
95 assert!(ctx.agent_type.is_none());
96 assert_eq!(ctx.agent_key(), "main");
97 }
98
99 const SUBAGENT_STOP_PAYLOAD: &str = r#"{
105 "session_id": "abc",
106 "transcript_path": "/t",
107 "cwd": "/c",
108 "hook_event_name": "SubagentStop",
109 "agent_id": "subagent-xyz",
110 "agent_type": "general-purpose"
111 }"#;
112
113 #[test]
114 fn deserializes_subagent_stop_payload() {
115 let ctx: HookContext = serde_json::from_str(SUBAGENT_STOP_PAYLOAD).unwrap();
116 assert_eq!(ctx.hook_event_name, HookEvent::SubagentStop);
117 assert_eq!(ctx.agent_id.as_deref(), Some("subagent-xyz"));
118 assert_eq!(ctx.agent_key(), "subagent-xyz");
119 assert_eq!(ctx.tool_name, "");
120 assert_eq!(ctx.tool_use_id, "");
121 }
122
123 #[test]
124 fn deserializes_subagent_payload() {
125 let ctx: HookContext = serde_json::from_str(SUBAGENT_PAYLOAD).unwrap();
126 assert_eq!(ctx.agent_id.as_deref(), Some("a56e70ccdc442bf74"));
127 assert_eq!(ctx.agent_type.as_deref(), Some("general-purpose"));
128 assert_eq!(ctx.agent_key(), "a56e70ccdc442bf74");
129 }
130}