lib.rsannotatedlib.rssource184 lines · 5.4 KB · raw

What the mod and the daemon both have to agree on: the hook events, what is done with each, and the messages that cross between them. The mod compiles this to JavaScript and the daemon natively, so the two cannot disagree about a name or a field.

6use serde::{Deserialize, Serialize};

Every Claude Code settings-hook event (the classic.<Event> names a mod 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}
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    ];

What is done with this event. Exhaustive, so a new event cannot be 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}

What is done with an event.

130#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
131#[serde(rename_all = "lowercase")]
132pub enum Role {

Sent to the daemon and waited on: the answer changes what happens.

134    Decide,

Sent to the daemon without waiting.

136    Observe,

Not sent.

138    Ignore,
139}

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 {

Run it without asking the user.

146    Allow,

Ask the user first (a tool call), or do not stop yet (a Stop).

148    Ask,

No opinion: whatever would have happened without this plugin happens.

150    Pass,
151}

The daemon's answer to a deciding event.

154#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
155pub struct Decision {
156    pub verdict: Verdict,

One line for the user: what Jev said the thing was and how sure it was, or why nothing was asked.

159    pub line: String,

How long the daemon took, Jev included.

161    pub ms: u64,

What this session's questions have cost so far, in dollars.

163    pub session_usd: f64,
164}
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}