1//! What the mod and the daemon both have to agree on: the hook events, what 2//! is done with each, and the messages that cross between them. The mod 3//! compiles this to JavaScript and the daemon natively, so the two cannot 4//! disagree about a name or a field. 5 6use serde::{Deserialize, Serialize}; 7 8/// Every Claude Code settings-hook event (the `classic.<Event>` names a mod 9/// hooks), as of Claude Code 2.1.287. 10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 11pub enum HookEvent { 12 PreToolUse, 13 PostToolUse, 14 PostToolUseFailure, 15 PostToolBatch, 16 PermissionDenied, 17 Notification, 18 UserPromptSubmit, 19 UserPromptExpansion, 20 SessionStart, 21 SessionEnd, 22 Stop, 23 StopFailure, 24 SubagentStart, 25 SubagentStop, 26 PreCompact, 27 PostCompact, 28 PreModelSwitch, 29 PostModelSwitch, 30 PermissionRequest, 31 Setup, 32 TeammateIdle, 33 TaskCreated, 34 TaskCompleted, 35 Elicitation, 36 ElicitationResult, 37 ConfigChange, 38 InstructionsLoaded, 39 WorktreeCreate, 40 WorktreeRemove, 41 CwdChanged, 42 FileChanged, 43 DirectoryAdded, 44 MessageDisplay, 45} 46 47impl HookEvent { 48 pub const ALL: [HookEvent; 33] = [ 49 Self::PreToolUse, 50 Self::PostToolUse, 51 Self::PostToolUseFailure, 52 Self::PostToolBatch, 53 Self::PermissionDenied, 54 Self::Notification, 55 Self::UserPromptSubmit, 56 Self::UserPromptExpansion, 57 Self::SessionStart, 58 Self::SessionEnd, 59 Self::Stop, 60 Self::StopFailure, 61 Self::SubagentStart, 62 Self::SubagentStop, 63 Self::PreCompact, 64 Self::PostCompact, 65 Self::PreModelSwitch, 66 Self::PostModelSwitch, 67 Self::PermissionRequest, 68 Self::Setup, 69 Self::TeammateIdle, 70 Self::TaskCreated, 71 Self::TaskCompleted, 72 Self::Elicitation, 73 Self::ElicitationResult, 74 Self::ConfigChange, 75 Self::InstructionsLoaded, 76 Self::WorktreeCreate, 77 Self::WorktreeRemove, 78 Self::CwdChanged, 79 Self::FileChanged, 80 Self::DirectoryAdded, 81 Self::MessageDisplay, 82 ]; 83 84 /// What is done with this event. Exhaustive, so a new event cannot be 85 /// added without deciding. 86 pub fn role(self) -> Role { 87 match self { 88 // Jev is asked and the answer changes what happens. 89 Self::PreToolUse | Self::Stop => Role::Decide, 90 // The daemon needs these to do the deciding: the prompt a Stop is 91 // judged against, what became of a command it judged, and the end 92 // of a session so its state is dropped. 93 Self::UserPromptSubmit 94 | Self::PostToolUse 95 | Self::PostToolUseFailure 96 | Self::PermissionDenied 97 | Self::SessionEnd => Role::Observe, 98 // Nothing here is judged or needed for a judgment yet. 99 Self::PostToolBatch 100 | Self::Notification 101 | Self::UserPromptExpansion 102 | Self::SessionStart 103 | Self::StopFailure 104 | Self::SubagentStart 105 | Self::SubagentStop 106 | Self::PreCompact 107 | Self::PostCompact 108 | Self::PreModelSwitch 109 | Self::PostModelSwitch 110 | Self::PermissionRequest 111 | Self::Setup 112 | Self::TeammateIdle 113 | Self::TaskCreated 114 | Self::TaskCompleted 115 | Self::Elicitation 116 | Self::ElicitationResult 117 | Self::ConfigChange 118 | Self::InstructionsLoaded 119 | Self::WorktreeCreate 120 | Self::WorktreeRemove 121 | Self::CwdChanged 122 | Self::FileChanged 123 | Self::DirectoryAdded 124 | Self::MessageDisplay => Role::Ignore, 125 } 126 } 127} 128 129/// What is done with an event. 130#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] 131#[serde(rename_all = "lowercase")] 132pub enum Role { 133 /// Sent to the daemon and waited on: the answer changes what happens. 134 Decide, 135 /// Sent to the daemon without waiting. 136 Observe, 137 /// Not sent. 138 Ignore, 139} 140 141/// What the daemon decided about a deciding event. 142#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] 143#[serde(rename_all = "lowercase")] 144pub enum Verdict { 145 /// Run it without asking the user. 146 Allow, 147 /// Ask the user first (a tool call), or do not stop yet (a Stop). 148 Ask, 149 /// No opinion: whatever would have happened without this plugin happens. 150 Pass, 151} 152 153/// The daemon's answer to a deciding event. 154#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 155pub struct Decision { 156 pub verdict: Verdict, 157 /// One line for the user: what Jev said the thing was and how sure it 158 /// was, or why nothing was asked. 159 pub line: String, 160 /// How long the daemon took, Jev included. 161 pub ms: u64, 162 /// What this session's questions have cost so far, in dollars. 163 pub session_usd: f64, 164} 165 166#[cfg(test)] 167mod tests { 168 use super::*; 169 170 #[test] 171 fn every_event_is_listed_once() { 172 let mut seen = std::collections::HashSet::new(); 173 for event in HookEvent::ALL { 174 assert!(seen.insert(event), "{event:?} twice"); 175 } 176 assert_eq!(seen.len(), 33); 177 } 178 179 #[test] 180 fn names_are_the_hook_names() { 181 assert_eq!(serde_json::to_string(&HookEvent::PreToolUse).unwrap(), "\"PreToolUse\""); 182 assert_eq!(serde_json::from_str::<HookEvent>("\"Stop\"").unwrap(), HookEvent::Stop); 183 } 184}