Skip to main content

forge_primitives/hooks/
inputs.rs

1//! Typed input payloads for each hook event.
2//!
3//! Every concrete
4//! input flattens [`BaseHookInput`] so callbacks can access `session_id`,
5//! `transcript_path`, `cwd`, and (optionally) `permission_mode` without
6//! reaching back into [`super::HookContext`].
7
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11/// Fields shared by every hook input the CLI emits. Every concrete
12/// input type below flattens this struct so callbacks can access
13/// `session_id`, `transcript_path`, `cwd`, and (optionally)
14/// `permission_mode` without reaching back into
15/// [`super::HookContext`].
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct BaseHookInput {
18    /// Session identifier the hook fired in.
19    pub session_id: String,
20    /// Filesystem path of the session transcript JSONL file.
21    pub transcript_path: String,
22    /// Working directory of the CLI when the hook fired.
23    pub cwd: String,
24    /// Permission mode active for the call (e.g. `"default"`,
25    /// `"bypassPermissions"`). Absent for hook frames that predate this
26    /// field or where the CLI omits it.
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub permission_mode: Option<String>,
29    /// Active effort level at the moment the hook fired (CLI 2.1.133+).
30    /// `None` for older CLI versions or hook frames that predate the
31    /// field. The wire shape is `{"effort": {"level": "max"}}` per the
32    /// 2.1.133 changelog ("Hooks now receive the active effort level
33    /// via the `effort.level` JSON input field"). Defensive Option lets
34    /// older baselines decode without the field present.
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    pub effort: Option<EffortInfo>,
37}
38
39/// Wrapper for the `effort` field on hook inputs. Currently only
40/// carries `level` but kept as a struct so future fields (budget,
41/// adaptive flags, …) can be added without breaking decode-side
42/// pattern matches.
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct EffortInfo {
45    /// Effort level string (`"low"` / `"medium"` / `"high"` /
46    /// `"xhigh"` / `"max"`). Kept as `String` rather than a typed
47    /// enum so future levels the CLI introduces don't fail decode;
48    /// consumers map to `crate::EffortLevel` when they need typed
49    /// access.
50    pub level: String,
51}
52
53/// Optional sub-agent attribution present on tool-lifecycle hook
54/// inputs (`PreToolUse`, `PostToolUse`, `PostToolUseFailure`,
55/// `PermissionRequest`). Fields are populated when the hook fires
56/// inside a `Task`-spawned sub-agent so tool events can be
57/// attributed back to the right agent when multiple sub-agents
58/// interleave over the same control channel.
59#[derive(Debug, Clone, Default, Serialize, Deserialize)]
60pub struct SubagentContext {
61    /// Sub-agent identifier. Matches the `agent_id` emitted by that
62    /// sub-agent's `SubagentStart` / `SubagentStop` hooks.
63    #[serde(default, skip_serializing_if = "Option::is_none")]
64    pub agent_id: Option<String>,
65    /// Agent type name (e.g. `"general-purpose"`, `"code-reviewer"`).
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub agent_type: Option<String>,
68}
69
70/// Input payload for `PreToolUse`.
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct PreToolUseInput {
73    /// Shared hook context (`session_id`, `transcript_path`, `cwd`, …).
74    #[serde(flatten)]
75    pub base: BaseHookInput,
76    /// Optional sub-agent attribution.
77    #[serde(flatten)]
78    pub subagent: SubagentContext,
79    /// Tool the model wants to invoke.
80    pub tool_name: String,
81    /// The model's proposed input.
82    pub tool_input: Value,
83    /// Opaque tool-use identifier the CLI will reference in the matching
84    /// `PostToolUse` / `PostToolUseFailure` frame.
85    pub tool_use_id: String,
86}
87
88/// Input payload for `PostToolUse`.
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct PostToolUseInput {
91    /// Shared hook context.
92    #[serde(flatten)]
93    pub base: BaseHookInput,
94    /// Optional sub-agent attribution.
95    #[serde(flatten)]
96    pub subagent: SubagentContext,
97    /// Tool that was invoked.
98    pub tool_name: String,
99    /// The input the tool actually ran with.
100    pub tool_input: Value,
101    /// The tool's response payload.
102    pub tool_response: Value,
103    /// Tool-use identifier matching the preceding `PreToolUse` frame.
104    pub tool_use_id: String,
105}
106
107/// Input payload for `PostToolUseFailure`.
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct PostToolUseFailureInput {
110    /// Shared hook context.
111    #[serde(flatten)]
112    pub base: BaseHookInput,
113    /// Optional sub-agent attribution.
114    #[serde(flatten)]
115    pub subagent: SubagentContext,
116    /// Tool that was invoked and failed.
117    pub tool_name: String,
118    /// Input the tool ran with.
119    pub tool_input: Value,
120    /// Tool-use identifier matching the preceding `PreToolUse` frame.
121    pub tool_use_id: String,
122    /// Error message the tool surfaced.
123    pub error: String,
124    /// `Some(true)` when the failure was caused by user interruption; `None`
125    /// when the CLI omits the field (upstream `NotRequired`).
126    #[serde(default, skip_serializing_if = "Option::is_none")]
127    pub is_interrupt: Option<bool>,
128}
129
130/// Input payload for `UserPromptSubmit`.
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct UserPromptSubmitInput {
133    /// Shared hook context.
134    #[serde(flatten)]
135    pub base: BaseHookInput,
136    /// Raw prompt text the user submitted.
137    pub prompt: String,
138}
139
140/// Input payload for `Stop`.
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct StopInput {
143    /// Shared hook context.
144    #[serde(flatten)]
145    pub base: BaseHookInput,
146    /// `true` when the stop-hook chain was previously active (re-entrant
147    /// call). Callbacks typically return `HookDecision::passthrough()`
148    /// when this is set to avoid infinite loops.
149    pub stop_hook_active: bool,
150}
151
152/// Input payload for `SubagentStop`.
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct SubagentStopInput {
155    /// Shared hook context.
156    #[serde(flatten)]
157    pub base: BaseHookInput,
158    /// Re-entrancy indicator (see [`StopInput::stop_hook_active`]).
159    pub stop_hook_active: bool,
160    /// Sub-agent identifier.
161    pub agent_id: String,
162    /// Filesystem path of the sub-agent's transcript JSONL file.
163    pub agent_transcript_path: String,
164    /// Agent type (e.g. `"general-purpose"`).
165    pub agent_type: String,
166}
167
168/// Input payload for `PreCompact`.
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct PreCompactInput {
171    /// Shared hook context.
172    #[serde(flatten)]
173    pub base: BaseHookInput,
174    /// Trigger that prompted the compaction - `"manual"` or `"auto"`.
175    pub trigger: String,
176    /// Caller-supplied compaction guidance. `None` when the CLI did not
177    /// pass custom instructions.
178    #[serde(default, skip_serializing_if = "Option::is_none")]
179    pub custom_instructions: Option<String>,
180}
181
182/// Input payload for `Notification`.
183#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct NotificationInput {
185    /// Shared hook context.
186    #[serde(flatten)]
187    pub base: BaseHookInput,
188    /// Body of the notification.
189    pub message: String,
190    /// Optional title; absent when the CLI omits it.
191    #[serde(default, skip_serializing_if = "Option::is_none")]
192    pub title: Option<String>,
193    /// Notification classification (e.g. `"permission_request"`).
194    pub notification_type: String,
195}
196
197/// Input payload for `SubagentStart`.
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct SubagentStartInput {
200    /// Shared hook context.
201    #[serde(flatten)]
202    pub base: BaseHookInput,
203    /// Sub-agent identifier for this spawn.
204    pub agent_id: String,
205    /// Agent type (e.g. `"general-purpose"`).
206    pub agent_type: String,
207}
208
209/// Input payload for `PermissionRequest`.
210///
211/// Observed when the CLI asks the SDK to confirm a tool invocation. This is
212/// distinct from the `can_use_tool` `control_request` path - `PermissionRequest`
213/// hooks are observational; actual allow/deny decisions still flow through
214/// `can_use_tool`.
215#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct PermissionRequestInput {
217    /// Shared hook context.
218    #[serde(flatten)]
219    pub base: BaseHookInput,
220    /// Optional sub-agent attribution.
221    #[serde(flatten)]
222    pub subagent: SubagentContext,
223    /// Tool the CLI is asking about.
224    pub tool_name: String,
225    /// Proposed tool input.
226    pub tool_input: Value,
227    /// Optional list of permission-rule suggestions the CLI proposes.
228    #[serde(default, skip_serializing_if = "Option::is_none")]
229    pub permission_suggestions: Option<Vec<Value>>,
230}