lib.rsannotatedlib.rssource184 lines · 5.4 KB · raw
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}