1use serde::{Deserialize, Deserializer, Serialize, Serializer};
9use serde_json::Value;
10use std::fmt;
11use std::io::Read;
12use std::path::Path;
13
14pub const STDIN_CAP: u64 = 4 * 1024 * 1024;
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
20pub enum Wire {
21 Claude,
22 Codex,
23}
24
25impl Wire {
26 pub fn parse(value: &str) -> Option<Self> {
27 match value {
28 "claude" => Some(Self::Claude),
29 "codex" => Some(Self::Codex),
30 _ => None,
31 }
32 }
33
34 pub const fn as_str(self) -> &'static str {
35 match self {
36 Self::Claude => "claude",
37 Self::Codex => "codex",
38 }
39 }
40}
41
42impl fmt::Display for Wire {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 f.write_str(self.as_str())
45 }
46}
47
48#[derive(Clone, Debug, PartialEq, Eq)]
51pub enum EventKind {
52 SessionStart,
53 UserPromptSubmit,
54 PreToolUse,
55 PostToolUse,
56 PostToolUseFailure,
57 Stop,
58 SubagentStart,
59 SubagentStop,
60 PreCompact,
61 PostCompact,
62 SessionEnd,
63 Notification,
64 Other(String),
65}
66
67impl EventKind {
68 pub fn from_name(name: &str) -> Self {
69 match name {
70 "SessionStart" | "session-start" => Self::SessionStart,
71 "UserPromptSubmit" | "user-prompt-submit" => Self::UserPromptSubmit,
72 "PreToolUse" | "pre-effect" => Self::PreToolUse,
73 "PostToolUse" | "post-effect" => Self::PostToolUse,
74 "PostToolUseFailure" | "post-effect-failure" => Self::PostToolUseFailure,
75 "Stop" | "stop" => Self::Stop,
76 "SubagentStart" | "subagent-start" => Self::SubagentStart,
77 "SubagentStop" | "subagent-stop" => Self::SubagentStop,
78 "PreCompact" | "pre-compact" => Self::PreCompact,
79 "PostCompact" | "post-compact" => Self::PostCompact,
80 "SessionEnd" => Self::SessionEnd,
81 "Notification" => Self::Notification,
82 other => Self::Other(other.to_owned()),
83 }
84 }
85
86 pub fn as_str(&self) -> &str {
87 match self {
88 Self::SessionStart => "SessionStart",
89 Self::UserPromptSubmit => "UserPromptSubmit",
90 Self::PreToolUse => "PreToolUse",
91 Self::PostToolUse => "PostToolUse",
92 Self::PostToolUseFailure => "PostToolUseFailure",
93 Self::Stop => "Stop",
94 Self::SubagentStart => "SubagentStart",
95 Self::SubagentStop => "SubagentStop",
96 Self::PreCompact => "PreCompact",
97 Self::PostCompact => "PostCompact",
98 Self::SessionEnd => "SessionEnd",
99 Self::Notification => "Notification",
100 Self::Other(name) => name,
101 }
102 }
103
104 pub fn takes_additional_context(&self) -> bool {
108 matches!(
109 self,
110 Self::SessionStart | Self::UserPromptSubmit | Self::PostToolUse
111 )
112 }
113}
114
115impl Serialize for EventKind {
116 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
117 serializer.serialize_str(self.as_str())
118 }
119}
120
121impl<'de> Deserialize<'de> for EventKind {
122 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
123 let name = String::deserialize(deserializer)?;
124 Ok(Self::from_name(&name))
125 }
126}
127
128impl fmt::Display for EventKind {
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130 f.write_str(self.as_str())
131 }
132}
133
134#[derive(Debug, Clone, PartialEq, Eq)]
136pub enum ParseError {
137 Malformed(String),
139 NotObject,
141 EventRequired,
143 TooLarge,
145 Io(String),
147}
148
149impl fmt::Display for ParseError {
150 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151 match self {
152 Self::Malformed(e) => write!(f, "hook payload is not JSON: {e}"),
153 Self::NotObject => f.write_str("HookHost payload must be an object"),
154 Self::EventRequired => f.write_str("HookHost event is required"),
155 Self::TooLarge => write!(f, "hook payload exceeds {STDIN_CAP} bytes"),
156 Self::Io(e) => write!(f, "hook payload could not be read: {e}"),
157 }
158 }
159}
160
161impl std::error::Error for ParseError {}
162
163#[derive(Clone, Debug, PartialEq)]
165pub struct HookEvent {
166 pub wire: Wire,
167 pub kind: EventKind,
168 pub session_id: Option<String>,
170 pub tool_name: Option<String>,
171 pub tool_use_id: Option<String>,
172 pub cwd: Option<String>,
173 pub transcript_path: Option<String>,
174 pub turn_id: Option<String>,
176 pub agent_id: Option<String>,
178 pub parent_tool_use_id: Option<String>,
179 pub prompt: Option<String>,
181 pub stop_hook_active: Option<bool>,
182 pub last_assistant_message: Option<String>,
183 pub tool_input: Option<Value>,
184 pub raw: Value,
186}
187
188fn first_string(object: &serde_json::Map<String, Value>, names: &[&str]) -> Option<String> {
189 names.iter().find_map(|name| {
190 object
191 .get(*name)
192 .and_then(Value::as_str)
193 .filter(|value| !value.trim().is_empty())
194 .map(str::to_owned)
195 })
196}
197
198impl HookEvent {
199 pub fn from_value(wire: Wire, payload: Value) -> Result<Self, ParseError> {
201 let object = payload.as_object().ok_or(ParseError::NotObject)?;
202 let name = first_string(object, &["hook_event_name", "hookEventName", "event"])
203 .ok_or(ParseError::EventRequired)?;
204 Ok(Self {
205 wire,
206 kind: EventKind::from_name(&name),
207 session_id: first_string(object, &["thread_id", "session_id", "sessionId"]),
208 tool_name: first_string(object, &["tool_name", "toolName"]),
209 tool_use_id: first_string(object, &["tool_use_id", "toolUseId"]),
210 cwd: first_string(object, &["cwd"]),
211 transcript_path: first_string(object, &["transcript_path"]),
212 turn_id: first_string(object, &["turn_id"]),
213 agent_id: first_string(object, &["agent_id"]),
214 parent_tool_use_id: first_string(object, &["parent_tool_use_id"]),
215 prompt: first_string(object, &["prompt", "user_prompt"]),
216 stop_hook_active: object.get("stop_hook_active").and_then(Value::as_bool),
217 last_assistant_message: first_string(object, &["last_assistant_message"]),
218 tool_input: object.get("tool_input").cloned(),
219 raw: payload,
220 })
221 }
222
223 pub fn from_slice(wire: Wire, bytes: &[u8]) -> Result<Self, ParseError> {
224 let value: Value =
225 serde_json::from_slice(bytes).map_err(|e| ParseError::Malformed(e.to_string()))?;
226 Self::from_value(wire, value)
227 }
228
229 pub fn from_reader(wire: Wire, reader: impl Read) -> Result<Self, ParseError> {
231 let mut raw = Vec::new();
232 reader
233 .take(STDIN_CAP + 1)
234 .read_to_end(&mut raw)
235 .map_err(|e| ParseError::Io(e.to_string()))?;
236 if raw.len() as u64 > STDIN_CAP {
237 return Err(ParseError::TooLarge);
238 }
239 Self::from_slice(wire, &raw)
240 }
241
242 pub fn is_own_turn(&self) -> bool {
247 if self.kind != EventKind::PostToolUse {
248 return true;
249 }
250 match self.wire {
251 Wire::Claude => self.agent_id.is_none(),
252 Wire::Codex => match (&self.session_id, &self.transcript_path) {
253 (Some(session), Some(path)) => Path::new(path)
254 .file_name()
255 .is_some_and(|name| name.to_string_lossy().contains(session.as_str())),
256 _ => false,
257 },
258 }
259 }
260}