Skip to main content

forge_primitives/
hooks.rs

1//! Hook callback wire-data - input payloads, output decisions, and
2//! the lifecycle plumbing.
3//!
4//! Workspace-shared shapes: `HookKind`, `HookContext`,
5//! `HookDecision`, plus every input/output payload. The callback
6//! machinery (`HookCallback` trait, `ErasedHookCallback`
7//! type-erasure adapter, `Hooks` builder/registry) stays SDK-side
8//! because it owns `Arc<dyn …>` pointers.
9//!
10//! Submodule split:
11//! - [`inputs`] - `BaseHookInput`, `SubagentContext`, the ten `*Input`
12//!   payload structs.
13//! - [`outputs`] - the `PreToolUse` / `UserPromptSubmit`
14//!   `hookSpecificOutput` wrappers and their `hookEventName` tag ZSTs.
15
16pub mod inputs;
17pub mod outputs;
18
19pub use inputs::{
20    BaseHookInput, NotificationInput, PermissionRequestInput, PostToolUseFailureInput,
21    PostToolUseInput, PreCompactInput, PreToolUseInput, StopInput, SubagentContext,
22    SubagentStartInput, SubagentStopInput, UserPromptSubmitInput,
23};
24pub use outputs::{PreToolUseHookSpecificOutput, UserPromptSubmitHookSpecificOutput};
25
26/// Identifies which hook point a callback is registered for. Sixteen event
27/// kinds mirrored from the CLI v0.1.64, plus
28/// `Unknown` as a fallback for forward-compatibility.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30pub enum HookKind {
31    /// Before a tool is invoked.
32    PreToolUse,
33    /// After a tool completes successfully.
34    PostToolUse,
35    /// After a tool errors.
36    PostToolUseFailure,
37    /// Every user prompt (can rewrite or cancel).
38    UserPromptSubmit,
39    /// End of an assistant turn.
40    Stop,
41    /// End of a sub-agent turn.
42    SubagentStop,
43    /// Start of a sub-agent turn.
44    SubagentStart,
45    /// Before session compaction.
46    PreCompact,
47    /// Out-of-band notification to the caller.
48    Notification,
49    /// Permission request observation (distinct from `can_use_tool`).
50    PermissionRequest,
51    /// Worktree created (LLM called EnterWorktree, or claude spawned with --worktree).
52    WorktreeCreate,
53    /// Worktree removed (LLM called ExitWorktree, or claude session ended).
54    WorktreeRemove,
55    /// Session cwd changed (LLM entered/exited a worktree).
56    CwdChanged,
57    /// External file modification observed by claude's file-history watcher.
58    FileChanged,
59    /// Session lifecycle: init.
60    SessionStart,
61    /// Session lifecycle: end.
62    SessionEnd,
63    /// Fallback for hook events forge-sdk doesn't yet recognise.
64    Unknown,
65}
66
67impl HookKind {
68    /// Wire-name used by the `claude` binary.
69    pub fn as_str(self) -> &'static str {
70        match self {
71            Self::PreToolUse => "PreToolUse",
72            Self::PostToolUse => "PostToolUse",
73            Self::PostToolUseFailure => "PostToolUseFailure",
74            Self::UserPromptSubmit => "UserPromptSubmit",
75            Self::Stop => "Stop",
76            Self::SubagentStop => "SubagentStop",
77            Self::SubagentStart => "SubagentStart",
78            Self::PreCompact => "PreCompact",
79            Self::Notification => "Notification",
80            Self::PermissionRequest => "PermissionRequest",
81            Self::WorktreeCreate => "WorktreeCreate",
82            Self::WorktreeRemove => "WorktreeRemove",
83            Self::CwdChanged => "CwdChanged",
84            Self::FileChanged => "FileChanged",
85            Self::SessionStart => "SessionStart",
86            Self::SessionEnd => "SessionEnd",
87            Self::Unknown => "Unknown",
88        }
89    }
90
91    /// Parse a wire-name back into the enum. Unknown strings fall through
92    /// to `HookKind::Unknown` - forge-sdk is forward-compatible with new
93    /// hook types Anthropic introduces between our parity checks.
94    pub fn from_wire(s: &str) -> Self {
95        match s {
96            "PreToolUse" => Self::PreToolUse,
97            "PostToolUse" => Self::PostToolUse,
98            "PostToolUseFailure" => Self::PostToolUseFailure,
99            "UserPromptSubmit" => Self::UserPromptSubmit,
100            "Stop" => Self::Stop,
101            "SubagentStop" => Self::SubagentStop,
102            "SubagentStart" => Self::SubagentStart,
103            "PreCompact" => Self::PreCompact,
104            "Notification" => Self::Notification,
105            "PermissionRequest" => Self::PermissionRequest,
106            "WorktreeCreate" => Self::WorktreeCreate,
107            "WorktreeRemove" => Self::WorktreeRemove,
108            "CwdChanged" => Self::CwdChanged,
109            "FileChanged" => Self::FileChanged,
110            "SessionStart" => Self::SessionStart,
111            "SessionEnd" => Self::SessionEnd,
112            _ => Self::Unknown,
113        }
114    }
115}
116
117/// Context carried alongside every hook invocation.
118#[derive(Debug, Clone)]
119pub struct HookContext {
120    /// Hook point being invoked.
121    pub kind: HookKind,
122    /// Tool name when applicable (`PreToolUse` / `PostToolUse`).
123    pub tool_name: Option<String>,
124    /// Session id.
125    pub session_id: String,
126    /// Tool-use id when the hook fired in a tool-use context
127    /// (`PreToolUse`, `PostToolUse`). `None` for other hook kinds.
128    pub tool_use_id: Option<String>,
129}
130
131#[cfg(test)]
132mod hook_kind_extension_tests {
133    use super::*;
134
135    #[test]
136    fn worktree_create_round_trips() {
137        assert_eq!(HookKind::from_wire("WorktreeCreate"), HookKind::WorktreeCreate);
138        assert_eq!(HookKind::WorktreeCreate.as_str(), "WorktreeCreate");
139    }
140
141    #[test]
142    fn worktree_remove_round_trips() {
143        assert_eq!(HookKind::from_wire("WorktreeRemove"), HookKind::WorktreeRemove);
144        assert_eq!(HookKind::WorktreeRemove.as_str(), "WorktreeRemove");
145    }
146
147    #[test]
148    fn cwd_changed_round_trips() {
149        assert_eq!(HookKind::from_wire("CwdChanged"), HookKind::CwdChanged);
150        assert_eq!(HookKind::CwdChanged.as_str(), "CwdChanged");
151    }
152
153    #[test]
154    fn file_changed_round_trips() {
155        assert_eq!(HookKind::from_wire("FileChanged"), HookKind::FileChanged);
156        assert_eq!(HookKind::FileChanged.as_str(), "FileChanged");
157    }
158
159    #[test]
160    fn session_start_round_trips() {
161        assert_eq!(HookKind::from_wire("SessionStart"), HookKind::SessionStart);
162        assert_eq!(HookKind::SessionStart.as_str(), "SessionStart");
163    }
164
165    #[test]
166    fn session_end_round_trips() {
167        assert_eq!(HookKind::from_wire("SessionEnd"), HookKind::SessionEnd);
168        assert_eq!(HookKind::SessionEnd.as_str(), "SessionEnd");
169    }
170
171    #[test]
172    fn truly_unknown_still_routes_to_unknown() {
173        assert_eq!(HookKind::from_wire("ThisIsNotARealEvent"), HookKind::Unknown);
174    }
175}